SwiftUI Text doesn't localise when inside ForEach - swift

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.

Related

Binding to subscript doesn't update TextField (macOS)

I have this Store struct, which is a wrapper for all my data. It has a subscript operator which takes in a UUID, and returns the associated object.
This way, I can have a List bind to a selection variable, which has type UUID, and then in another view I can access the selected object from that UUID.
However, I'm experiencing an issue where my TextField which binds to the Store doesn't update. It does update if I wrap it in another Binding, or if I instead just use Text.
Here is an minimal reproducible example:
struct Person: Identifiable, Hashable {
let id = UUID()
var name: String
}
struct Store {
var data: [Person]
subscript(id: Person.ID) -> Person {
get {
data.first(where: { $0.id == id })!
}
set {
data[data.firstIndex(where: { $0.id == id })!] = newValue
}
}
}
struct ContentView: View {
#State var store = Store(data: [
Person(name: "Joe"),
Person(name: "Eva"),
Person(name: "Sam"),
Person(name: "Mary")
])
#State var selection: Person.ID?
var body: some View {
NavigationView {
List(store.data, selection: $selection) {
Text($0.name)
}
if let selection = selection {
// Creating a new Binding which simply wraps $store[selection].name
// fixes this issue. Or just using Text also works.
TextField("Placeholder", text: $store[selection].name)
}
else {
Text("No Selection")
}
}
}
}
To reproduce this issue, just click different names on the Sidebar. For some reason the detail view's TextField doesn't update!
This issue can also be resolved if we simply move the Store to a ObservableObject class with #Published.
Also, making the Store conform to Hashable doesn't help this issue.
I feel like I'm missing something very basic with SwiftUI. Is there any way to fix this?
EDIT:
I've changed out Store for an [Person], and I made an extension with the same subscript operator that is in Store. However, the problem still remains!
try this:
TextField("Placeholder", text: $store[selection].name)
.id(selection) // <-- here

Is it possible to instantiate a SwiftUI view from a string?

Is it possible to convert a string to a SwiftUI view? Something like JavaScript's eval(). In the code below I'm looking for an XXX function.
let uiString: String = “VStack { Text(\“hi there\”) }”
let view: some View = XXX(uiString)
!!! Not recommended !!! Do not try This at home!
Here is an example, but you need to consider any possible view, or may you just limit your work for just some special input, like I did.
PS: You can do some big optimization in codes to make it better and faster with using some more for loop, switch or enums, but at the end of the day, the things that I showed here is work of SwiftUI not us! And not recommended. It was just a show case that how code be done.
struct ContentView: View {
var body: some View {
StringDecoderView(string: "Circle().fill(Color.red); Rectangle()")
}
}
struct StringDecoderView: View {
let string: String
var body: some View {
let arrayOfComponents: [String] = string.components(separatedBy: ";")
var anyView: [CustomViewType] = [CustomViewType]()
for item in arrayOfComponents {
if item.contains("Circle()") {
if item.contains(".fill") && item.contains("Color.red") {
anyView.append(CustomViewType(anyView: AnyView(Circle().fill(Color.red))))
}
else {
anyView.append(CustomViewType(anyView: AnyView(Circle())))
}
}
else if item.contains("Rectangle()") {
anyView.append(CustomViewType(anyView: AnyView(Rectangle())))
}
}
return ForEach(anyView) { item in
item.anyView
}
.padding()
}
}
struct CustomViewType: Identifiable {
let id: UUID = UUID()
var anyView: AnyView
}
No, because Swift is compiled to machine code. There isn't an interpreter to evaluate arbitrary expressions on the go.
That sounds a lot like a security vulnerability. Why would you want to do that?

Declare an array of structs whose content conforms to View protocol

Prefix: I'm a newbie regarding Generics in Swift. That's why the following problem and the resulting compiler messages are hard to understand for me.
Working with SwiftUI, I made a struct 'AlignedForm' that should use an Array to hold several 'AlignedFormRow' structs. These AlignedFormRows consist of a text label on the right and an arbitrary view on the left. They are implemented as a generic type like this:
struct AlignedFormRow<Content>: View where Content: View {
let content: Content
let title: String
let titleWidth: CGFloat
init(_ title: String, titleWidth: CGFloat, #ViewBuilder content: #escaping () -> Content) {
self.title = title
self.titleWidth = titleWidth
self.content = content()
}
var body: some View {
HStack {
Text(title).frame(width: titleWidth, alignment: .trailing)
content
}
}
}
Now, I can create content for my form rows in nice SwiftUI-style like this:
VStack {
AlignedFormRow("Full Name", titleWidth:100.0) {
TextField("your name", text: $name)
}
AlignedFormRow("Email", titleWidth:100.0) {
TextField("your email", text: $email)
}
}
Which results in two aligned labelled text fields:
So far it works fine. Now, I would like to create a helper struct "AlignedForm" that manages the titleWidth for all rows. But when I try to implement AlignedForm, I'm running into problems declaring a 'rows' Array:
struct AlignedForm {
let rows: [AlignedFormRow<Content: View>]
}
The code doesn't compile and the compiler responds to that line with "Expected '>' to complete generic argument list" which doesn't make much sense to me. I blindly tried many different ways to declare this rows array, but it never compiles. What's the correct syntax here? Or is this approach flawed otherwise?
I don't know why you wrote AlignedFormRow<Content: View>.
First you need to create a struct that conforms to View protocol, then use it in the declaration. Like,
struct AlignedFormView: View {
var body: some View {
EmptyView()
}
}
Then, use it in your array.
struct AlignedForm {
let rows: [AlignedFormRow<AlignedFormView>]
}
This is the way. Thank you

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

SwiftUI Picker with selection as struct

Iam trying to use Picker as selection of struct. Let say I have a struct "Pet" like below
struct Pet: Identifiable, Codable, Hashable {
let id = UUID()
let name: String
let age: Int
}
I am getting all Pet's from some class, where Pets are defined as #Published var pets = Pet
static let pets = Class().pets
I would like to be able to write a selection from picker to below variable:
#State private var petSelection: Pet?
Picker is:
Picker("Pet", selection: $petSelection){
ForEach(Self.pets) { item in
Text(item.name)
}
}
Picker shows properly all avaliavble pets but when I chose one petSelection has been not changed (nil). How should I mange it?
Thanks!
Edit:
Of course I know that I can use tag like below:
Picker("Pet", selection: $petSelection) {
ForEach(0 ..< Self.pet.count) { index in
Text(Self.pet[index].name).tag(index)
}
But wonder is it possible to use struct as selection. Thanks
Short answer: The type associated with the tag of the entries in your Picker (the Texts) must be identical to the type used for storing the selection.
In your example: You have an optional selection (probably to allow "empty selection") of Pet?, but the array passed to ForEach is of type [Pet]. You have to add therefore a .tag(item as Pet?) to your entries to ensure the selection works.
ForEach(Self.pets) { item in
Text(item.name).tag(item as Pet?)
}
Here follows my initial, alternate answer (getting rid of the optionality):
You have defined your selection as an Optional of your struct: Pet?. It seems that the Picker cannot handle Optional structs properly as its selection type.
As soon as you get rid of the optional for example by introducing a "dummy/none-selected Pet", Picker starts working again:
extension Pet {
static let emptySelection = Pet(name: "", age: -1)
}
in your view initialise the selection:
#State private var petSelection: Pet = .emptySelection
I hope this helps you too.
You use the following way:
#Published var pets: [Pet?] = [ nil, Pet(name: "123", age: 23), Pet(name: "123dd", age: 243),]
VStack{
Text(petSelection?.name ?? "name")
Picker("Pet", selection: $petSelection){
ForEach(Self.pets, id: \.self) { item in
Text(item?.name ?? "name").tag(item)
}}}
the type of $petSelection in Picker(selection:[...] has to be the same type of id within your struct.
So in your case you would have to change $petSelection to type if UUID since your items within the collection have UUID as identifier.
Anyway since this is not what you're after, but your intention is to receive the Pet as a whole when selected. For that you will need a wrapper containing Pet as the id. Since Pet is already Identifiable, there're only a few adjustments to do:
Create a wrapper having Pet as an id
struct PetPickerItem {
let pet: Pet?
}
Now wrap all collection items within the picker item
Picker("Pet", selection: $petSelection) {
ForEach(Self.pets.map(PetPickerItem.init), id: \.pet) {
Text("\($0.pet?.name ?? "None")")
}
}
You can now do minor adjustments like making PetPickerItem identifiable to remove the parameter id: from ForEach.
That's the best solution I came up with.
This is how I do it:
struct Language: Identifiable, Hashable {
var title: String
var id: String
}
struct PickerView: View {
var languages: [Language] = [Language(title: "English", id: "en-US"), Language(title: "German", id: "de-DE"), Language(title: "Korean", id: "ko-KR")]
#State private var selectedLanguage = Language(title: "German", id: "de-DE")
var body: some View {
Picker(selection: $selectedLanguage, label: Text("Front Description")) {
ForEach(self.languages, id: \.self) {language in
Text(language.title)
}
}
}