How to set canvas preview language? - swift

(currently trying on Xcode 11 Beta 7)
I want to pass an already localized string to Text() and see how it looks on canvas using ".environment(.locale, .init(identifier:"ja"))", but the preview is always set to whatever language that I have set on the scheme settings.
I know that it works if I pass a LocalizedStringKey directly, like Text("introTitle"), but I do not want to do that. Instead I want to use enums, like Text(L10n.Intro.title), but when I do that the environment operator is overriden by the scheme settings language.
Is this a bug or expected behaviour?
struct ContentView: View {
var body: some View {
Text("introTitle") //this works
Text(L10n.Intro.title) //this doesn't
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ForEach(["en", "ja", "pt"], id: \.self) { localeIdentifier in
ContentView()
.environment(\.locale, .init(identifier: localeIdentifier)) //this gets ignored, and only the scheme settings language is previewed
.previewDisplayName(localeIdentifier)
}
}
}
internal enum L10n {
internal enum Intro {
internal static let title = NSLocalizedString("introTitle", comment: "")
internal static let title2 = "introTitle" //this also doesn't work
}
}
In Localizable.strings I have:
//english
"introTitle" = "Welcome!";
//japanese
"introTitle" = "ようこそ!";
//portuguese
"introTitle" = "Bem-vindo(a)!";

My preferred approach would be to use Text("introTitle"), but if you want to use enums for your localised keys, you have to declare it like this
internal enum L10n {
internal enum Intro {
internal static let title = LocalizedStringKey("introTitle")
}
}
and then you will be able to use it like this:
Text(L10n.Intro.title)
In your code title is of type NSLocalisedString, title2 is a String, but you need a LocalizedStringKey to pass into Text initialiser.

Related

SwiftUI ShareLink share custom data

I am trying to share a custom struct between two users within the App via ShareLink (UIActivityViewController) in SwiftUI. I created a custom Document Type, the Exported Type Identifier and marked the struct as Transferable.
However, while I am able to save the example to a file, I would like to send it to another user via AirDrop or similar, s.t. the App appears as an Application Activity.
import SwiftUI
import UniformTypeIdentifiers
struct SwiftUIView: View {
#State private var customStruct: CustomStruct = CustomStruct()
var body: some View {
ShareLink(item: customStruct, preview: SharePreview(customStruct.name))
}
}
struct CustomStruct: Codable {
var name: String = "Test Example"
var description: String = "Test"
}
extension CustomStruct: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .customStruct)
}
}
extension UTType {
static var customStruct: UTType { UTType(exportedAs: "com.TestExample.CustomStruct") }
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
Have you tried setting the "Conforms To" value in "Exported Type Identifiers" to public.json instead of public.data? After all, your custom struct conforms to Codable and can thus be serialized into JSON. For me this did the trick and Messages started showing up in the share sheet. When running on a device, I also see AirDrop.
What I’m still struggling with is how the receiving app can actually handle the file. I added the LSSupportsOpeningDocumentsInPlace key to Info.plist and set it to YES which causes the file to open in my app by tapping, but then I’m a bit stuck. Maybe onOpenURL: and decode the JSON file that the URL points to? I found zero results on the internet for this, which is a bit surprising.

How to get the language code back from a localizedString?

I have a Picker in my Settings tab that displays all the supported languages to the user. Showing the language codes is not very human-readable, so I display the localizedStrings in the Picker instead. I need to retrieve the original language code back from this localizedString, however, to store it in the UserDefault (or in this case, through the #AppStorage). Is there a way to do this through Locale (or any other built-in library)? I read the documentation and tried looking for similar questions on StackOverflow / the Apple developer forum, but all questions seem to be about language code --> localizedString, rather than localizedString -> language code. Alternatively, I can also make an enum that stores all this information for me, but I'd like to know whether there's a better way of doing this.
The code:
#AppStorage("language") var language: String = "en"
#State var selectedLanguage = "English"
Picker("settingsTabGeneralSectionHeader".localized(), selection: $selectedLanguage) {
ForEach(Bundle.main.localizations, id: \.self) {
Text((Locale.current as NSLocale).localizedString(forLanguageCode: $0)!)
}
}
.onChange(of: selectedLanguage) { selection in
language = ??? // Inverse of Locale.localizedString
}
We can bind Picker selection directly to AppStorage and use code as tag to match, so code is simplified to
struct ContentView: View {
#AppStorage("language") var language: String = "en"
var body: some View {
VStack {
Text("Selected: " + language)
Picker("", selection: $language) {
ForEach(Bundle.main.localizations, id: \.self) {
Text((Locale.current as NSLocale)
.localizedString(forLanguageCode: $0)!).tag($0) // << here !!
}
}
}
}
}
and selection separated from presentation.
Tested with Xcode 13.4 / iOS 15.5
Test module on GitHub

Swift & SwiftUI - Conditional global var

I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}

SwiftUI Text doesn't localise when inside ForEach

When my Text gets it's string from an array, it doesn't localise, but when I hard-code it in, it does:
Localizable.strings
"greeting1" = "Hello there";
"greeting2" = "Howdy";
(Localised into English)
My View
var localisedStrings = ["greeting1", "greeting2"]
struct Message: View {
var body: some View {
ForEach(localisedStrings, id: \.self) { string in
Text(string)
}
}
}
The text will just write the key, like
"greeting1", rather than "Hello there"
Is there a way to force the Text object to use a localised string? Or is it changing somehow because it's getting it's content from a ForEach loop?
My View (hard coded)
//var localisedStrings = ["greeting1", "greeting2"]
struct Message: View {
var body: some View {
//ForEach(localisedStrings, id: \.self) { string in
Text("greeting1")
//}
}
}
This works just fine, and results in the text displaying "Hello there".
It works when you hardcode the string in, because you are using this initialiser, which takes a LocalizedStringKey.
LocalizedStringKey is ExpressibleByStringLiteral, but the word string in Text(string) is not a "string literal", so it calls this initialiser instead, and is not localised. You can initialise an instance of LocalizedStringKey directly, in order to localise it:
Text(LocalizedStringKey(string))
It's because here you are using plain String Swift type
var localisedStrings = ["greeting1", "greeting2"]
and here you are using SwiftUI own LocalizedStringKey type
Text("greeting1")
To fix your issue map your string to LocalizedStringKey
struct Message: View {
var body: some View {
ForEach(localisedStrings, id: \.self) { string in
Text(LocalizedStringKey(string))
}
}
}
Text.init(_:tableName:bundle:comment:)
Please follow the linked documentation to learn more.

Access and modify a #EnvironmentObject on global functions

I have an ObservableObject declared on my main view (ContentView.swift).
final class DataModel: ObservableObject {
#AppStorage("stuff") public var notes: [NoteItem] = []
}
Then I declare it in the main entry of the app as (removed extra code not needed for this example):
#main struct The_NoteApp: App {
private let dataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
}
In the ContentView.swift, I can use it on the different views I declared there:
struct NoteView: View {
#EnvironmentObject private var data: DataModel
// more code follows...
}
Now, I have a collection of global functions saved on FileFunctions.swift, which essentially are functions that interact with files on disk. One of them is to load those files and their content into my app.
Now, I'm trying to use #EnvironmentObject private var data: DataModel in those functions so at loading time, I can populate the data model with the actual data from the files. And when I declare that either as a global declaration in FileFunctions.swift or inside each function separately, I get two behaviors.
With the first one I get an error:
Global 'var' declaration requires an initializer expression or an explicitly stated getter`,
and
Property wrappers are not yet supported in top-level code
I tried to initialize it in any way, but it goes nowhere. With the second one, adding them to each function, Xcode craps on me with a segfault. Even if I remove the private and try to declare it in different ways, I get nowhere.
I tried the solution in Access environment variable inside global function - SwiftUI + CoreData, but the more I move things around the worse it gets.
So, how would I access this ObservableObject, and how would I be able to modify it within global functions?
Below is an example of a global function and how it's being called.
In FileFunctions.swift I have:
func loadFiles() {
var text: String = ""
var title: String = ""
var date: Date
do {
let directoryURL = try resolveURL(for: "savedDirectory")
if directoryURL.startAccessingSecurityScopedResource() {
let contents = try FileManager.default.contentsOfDirectory(at: directoryURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles])
for file in contents {
text = readFile(filename: file.path)
date = getModifiedDate(filename: file.absoluteURL)
title = text.components(separatedBy: NSCharacterSet.newlines).first!
// I need to save this info to the DataModel here
}
directoryURL.stopAccessingSecurityScopedResource()
} else {
Alert(title: Text("Couldn't load notes"),
message: Text("Make sure the directory where the notes are stored is accessible."),
dismissButton: .default(Text("OK")))
}
} catch let error as ResolveError {
print("Resolve error:", error)
return
} catch {
print(error)
return
}
}
And I call this function from here:
#main struct The_NoteApp: App {
private let dataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
.onAppear {
loadFiles()
}
}
}
You could change the signature of the global functions to allow receiving the model:
func loadFiles(dataModel: DataModel) { ... }
This way, you have access to the model instance within the function, what's left to do is to pass it at the call site:
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
.onAppear {
loadFiles(dataModel: self.dataModel)
}
You can do the same if the global functions calls originate from the views.
I would do something like this :
final class DataModel: ObservableObject {
public static let shared = DataModel()
#AppStorage("stuff") public var notes: [NoteItem] = []
}
#main struct The_NoteApp: App {
private let dataModel = DataModel.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
}
now in your viewModel you can access it like this
class AnyClass {
init (){
print(DataModel.shared.notes)
}
// or
func printNotes(){
print(DataModel.shared.notes)
}
}
As discussed in the comments, here a basic approach which makes some changes to the structure by defining dedicated "components" which have a certain role and which are decoupled as far as necessary.
I usually define a namespace for a "feature" where I put every "component" which is related to it. This offers a couple of advantages which you might recognise soon later:
enum FilesInfo {}
Using a "DataModel" or a "ViewModel" to separate your "Data" from the View
makes sense. A ViewModel - as opposed to DataModel - just obeys the rules from the MVVM pattern. A ViewModel should expose a "binding". I call this "ViewState", which completely describes what the view should render:
extension FilesInfo {
enum ViewState {
struct FileInfo {
var date: Date
var title: String
}
case undefined
case idle([FileInfo])
init() { self = .undefined } // note that!
}
}
Why ViewState is an enum?
Because you might want to represent also a loading state when your load function is asynchronous (almost always the case!) and an error state later. As you can see, you start with a state that's "undefined". You can name it also "zero" or "start", or however you like. It just means: "no data loaded yet".
A view model basically looks like this:
extension FilesInfo {
final class ViewModel: ObservableObject {
#Published private(set) var viewState: ViewState = .init()
...
}
}
Note, that there is a default initialiser for ViewState.
It also may have public functions where you can send "events" to it, which may originate in the view, or elsewhere:
extension FilesInfo.ViewModel {
// gets the view model started.
func load() -> Void {
...
}
// func someAction(with parameter: Param) -> Void
}
Here, the View Model implements load() - possibly in a similar fashion you implemented your loadFiles.
Almost always, a ViewModel operates (like an Actor) on an internal "State", which is not always the same as the ViewState. But your ViewState is a function of the State:
extension FilesInfo.ViewModel {
private struct State {
...
}
private func view(_ state: State) -> ViewState {
//should be a pure function (only depend on state variable)
// Here, you likely just transform the FilesInfo to
// something which is more appropriate to get rendered.
// You call this function whenever the internal state
// changes, and assign the result to the published
// property.
}
}
Now you can define your FileInfosView:
extension FilesInfo {
struct ContentView: View {
let state: ViewState
let action: () -> Void // an "event" function
let requireData: () -> Void // a "require data" event
var body: some View {
...
.onAppear {
if case .undefined = state {
requireData()
}
}
}
}
}
When you look more closely on the ContentView, it has no knowledge from a ViewModel, neither from loadFiles. It only knows about the "ViewState" and it just renders this. It also has no knowledge when the view model is ready, or provides data. But it knows when it should render data but has none and then calls requireData().
Note, it does not take a ViewModel as parameter. Those kind of setups are better done in some dedicated parent view:
extension FilesInfo {
struct CoordinatorView: View {
#ObservedObject viewModel: ViewModel
var body: some View {
ContentView(
state: viewModel.viewState,
action: {},
requireData: viewModel.load
)
}
}
}
Your "coordinator view" deals with separating ViewModel from your specific content view. This is not strictly necessary, but it increases decoupling and you can reuse your ContentView elsewhere with a different ViewModel.
Your CoordinatorView may also be responsible for creating the ViewModel and creating target views for navigation. This depends on what convention you establish.
IMHO, it may make sense, to restrict the access to environment variables to views with a certain role, because this creates a dependency from the view to the environment. We should avoid such coupling.
Also, I would consider mutating environment variables from within Views a "smell". Environment variables should be kind of a configuration which you setup in a certain place in your app (also called "CompositionRoot"). You may end up with an uncontrollable net of variables if you allow that everyone can change any environment variable at any time. When you have "ViewModels" in your environment, these of course get not "mutated" when they change their state - these are classes - for a reason.
Basically, that's it for a very basic but functional MVVM pattern.