Declare an array of structs whose content conforms to View protocol - swift

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

Related

How to define a new ForEach struct to implement the index function while keeping its original syntax in Swift?

I'm trying to define a new struct called ForEach to implement the index function which has the similar syntax to the SwiftUI ForEach.
struct ForEach<Data: RandomAccessCollection,Content: View>: View where Data.Element: Identifiable, Data.Element: Hashable {
let data: Data
#ViewBuilder let content: (Data.Index, Data.Element) -> Content
init(_ data: Data, content: #escaping (Data.Index, Data.Element) -> Content) {
self.data = data
self.content = content
}
var body: some View {
SwiftUI.ForEach(Array(zip(data.indices, data)), id: \.1) { index, element in
content(index, element)
}
}
}
ForEach(array) { index, item in
// each item here
}
But after I define my custom ForEach. The original syntax doesn't work.
ForEach(array) { item in // Doesn't work after defining my custom ForEach
}
For code reusing reason, I must keep the original syntax. So simply replacing index with _ to avoid error doesn't work. How can I fix this? Or any alternative to the same function?
Do not use this. It will confuse people because nothing is mentioning what the id is.
This is what you were looking for:
https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md
ForEach(collection.indexed(), id: \.index) {
$0.index
$0.element

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

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.

How to translate / localize a variable in Swift

I have this code and I have localization setup successfully. I created a Categories.strings file and localized it, but the translation of the categories is not working.
Code:
struct CategoriesViewRow: View {
var selection: SimpleJson
var body: some View {
HStack {
Image(selection.name)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text(String(localized: "\(selection.name)", table: "Categories", comment: "Category"))
Spacer()
}
}
}
This var selection: SimpleJson is a struct that looks like this:
struct SimpleJson: Hashable, Codable, Identifiable {
var id: Int
var name: String
var description: String?
var hidden: Bool?
}
It contains some static categories loaded from a file. They need to be loaded from the file, and I always managed to translate them in Objective-C. But now in Swift, it seems like I am missing something.
Normal SwiftUI Text() code gets translated correctly, but with my other code, there is something wrong.
I am currently using Xcode Beta 13.0, but I am quite sure this isn't the problem. I rather think I am doing something wrong, but I can't seem to find some good info for my use-case.
Can someone help me out?
Thanks in advance
You can add this function to a String extension and then call .localize() on string variables:
extension String {
/**
So that when you return a String it also adjusts the language
- Parameter comment: To describe specific meaning if it's unclear (e.g.: bear could mean the animal or the verb to bear)
- Returns: The localized String duh
- Usage: str.localize()
- Further reading: https://www.hackingwithswift.com/example-code/uikit/how-to-localize-your-ios-app
*/
func localize(comment: String = "") -> String {
NSLocalizedString(self, comment: comment)
}
}

Using ForEach with a an array of Bindings (SwiftUI)

My objective is to dynamically generate a form from JSON. I've got everything put together except for generating the FormField views (TextField based) with bindings to a dynamically generated list of view models.
If I swap out the FormField views for just normal Text views it works fine (see screenshot):
ForEach(viewModel.viewModels) { vm in
Text(vm.placeholder)
}
for
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: $vm)
}
I've tried to make the viewModels property of ConfigurableFormViewModel an #State var, but it loses its codability. JSON > Binding<[FormFieldViewModel] naturally doesn't really work.
Here's the gist of my code:
The first thing that you can try is this:
ForEach(0 ..< numberOfItems) { index in
HStack {
TextField("PlaceHolder", text: Binding(
get: { return items[index] },
set: { (newValue) in return self.items[index] = newValue}
))
}
}
The problem with the previous approach is that if numberOfItems is some how dynamic and could change because of an action of a Button for example, it is not going to work and it is going to throw the following error: ForEach<Range<Int>, Int, HStack<TextField<Text>>> count (3) != its initial count (0). 'ForEach(_:content:)' should only be used for *constant* data. Instead conform data to 'Identifiable' or use 'ForEach(_:id:content:)' and provide an explicit 'id'!
If you have that use case, you can do something like this, it will work even if the items are increasing or decreasing during the lifecycle of the SwiftView:
ForEach(items.indices, id:\.self ){ index in
HStack {
TextField("PlaceHolder", text: Binding(
get: { return items[index] },
set: { (newValue) in return self.items[index] = newValue}
))
}
}
Trying a different approach. The FormField maintains it's own internal state and publishes (via completion) when its text is committed:
struct FormField : View {
#State private var output: String = ""
let viewModel: FormFieldViewModel
var didUpdateText: (String) -> ()
var body: some View {
VStack {
TextField($output, placeholder: Text(viewModel.placeholder), onCommit: {
self.didUpdateText(self.output)
})
Line(color: Color.lightGray)
}.padding()
}
}
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: vm) { (output) in
vm.output = output
}
}
Swift 5.5
From Swift 5.5 version, you can use binding array directly by passing in the bindable like this.
ForEach($viewModel.viewModels, id: \.self) { $vm in
FormField(viewModel: $vm)
}
A solution could be the following:
ForEach(viewModel.viewModels.indices, id: \.self) { idx in
FormField(viewModel: self.$viewModel.viewModels[idx])
}
Took some time to figure out a solution to this puzzle. IMHO, it's a major omission, especially with SwiftUI Apps proposing documents that has models in struct and using Binding to detect changes.
It's not cute, and it takes a lot of CPU time, so I would not use this for large arrays, but this actually has the intended result, and, unless someone points out an error, it follows the intent of the ForEach limitation, which is to only reuse if the Identifiable element is identical.
ForEach(viewModel.viewModels) { vm in
ViewBuilder.buildBlock(viewModel.viewModels.firstIndex(of: zone) == nil
? ViewBuilder.buildEither(first: Spacer())
: ViewBuilder.buildEither(second: FormField(viewModel: $viewModel.viewModels[viewModel.viewModels.firstIndex(of: vm)!])))
}
For reference, the ViewBuilder.buildBlock idiom can be done in the root of the body element, but if you prefer, you can put this with an if.