Understanding list identifiers in SwiftUI - swift

Apple tutorial:
Lists work with identifiable data. You can make your data identifiable in one of two ways: by passing along with your data a key path to a property that uniquely identifies each element, or by making your data type conform to the Identifiable protocol.
I am curious what the implications are for lists that show items by design that are semantically "equal", creating duplicate rows that should behave in the same manner (i.e. both be deleted on removal by id). For instance:
List(["a", "b", "b", "c", "c"], id: \.self) { str in
Text(str)
}
I think I saw some sources saying that each row must be uniquely identified. Is that really true or should it be identifi-able?
The code above doesn't seem to crash and works fine -- is it actually fine?

The code above doesn't seem to crash and works fine -- is it actually
fine?
Yes! Definitely, List and ForEach needs their item's be in some way identifiable. What does it means for us? That means we should provide unique items to List or ForEach! But you would say wait a moment I did not make my array or items unique, and instead of that I made even duplicate items as well, So why it does worked?
Well I would say you absolutely made all your items unique, in this way that only 1 of them exist not 2 or . . ., you would say how? the answer is in id: \.self when you use id: \.self term you are literally saying and conforming to Integer Indexing ID system from SwiftUI, with using id: \.self SwiftUI understand that you are not going show a way for SwiftUI to understand which item is which, then it start to work by itself, and given an undercover id to your items.
So you can even delete the part of: id: \.self from your code, if you can show a way to SwiftUI to make your items Identifiable, the way that SwiftUI works in this case is awesome, when you remove id: \.self it starts analysing the type of item in your List/ForEach, then automatically goes to search identifiable protocol in that type, So that means your CustomType also should conform identifiable.
Let me gave you an example that we are going delete id: \.self and even making more duplicate and also deleting items, all would be identifiable.
import SwiftUI
struct ContentView: View {
#State private var array: Array<String> = ["a", "a", "a", "b", "b", "b", "c", "c", "c"]
var body: some View {
List(array) { item in
Text(item)
}
Button("remove last item") {
if array.count > 0 { array.remove(at: array.count - 1) }
}
.foregroundColor(Color.red)
.font(Font.body.weight(Font.Weight.bold))
}
}
extension String: Identifiable {
public var id: UUID {
get {
return UUID()
}
}
}

Related

SwiftUI Initialzier requires String conform to Identifiable

I am attempting writing some SwiftUI code for the very first time and am running into an issue while trying to make a simple table view. In this code teams is an array of String, but the List is giving me the following error: Initializer 'init(_:rowContent:)' requires that 'String' conform to 'Identifiable'
var body: some View {
let teams = getTeams()
Text("Hello there")
List(teams) { team in
}
}
Anyone have an idea what it means?
The argument of List is required to be unique, that's the purpose of the Identifiable protocol
You can do
List(teams, id: \.self) { team in
but then you are responsible to ensure that the strings are unique. If not you will get unexpected behavior.
Better is to use a custom struct with an unique id conforming to Identifiable
The Apple preferred way to do this is to add a String extension:
extension String: Identifiable {
public typealias ID = Int
public var id: Int {
return hash
}
}
See this code sample for reference:
https://github.com/apple/cloudkit-sample-queries/blob/main/Queries/ContentView.swift
I'm assuming that getTeams() returns a [String] (a.k.a. an Array<String>).
The issue, as the error suggests, are the strings aren't identifiable. But not just in the "they don't conform to the Identifiable protocol" sense, but in the broader sense: if two strings are "foo", one is indistinguishable from the other; they're identical.
This is unacceptable for a List view. Imagine you had a list that contains "foo" entries, and the users dragged to rearrange one of them. How would List be able to tell them apart, to know which of the two should move?
To get around this, you need to use an alternate source of identity to allow the List to distinguish all of its entries.
You should watch the "Demystify SwiftUI" talk from this year's WWDC. It goes into lots of detail on exactly this topic.
If your trying to go over a 'List' of items of type 'Struct'
Change the Struct to use 'Identifiable':
my Struct:
struct Post : Identifiable{
let id = UUID()
let title: String
}
my List :
let posts = [
Post( title: "hellow"),
Post( title: "I am"),
Post( title: "doing"),
Post( title: "nothing")
]
then you can go over your List:
List(posts){post in
Text(post.title
}

SwiftUI Getting Unique Values from a Struct (from a JSON)

I'm new to Swift and stuck on what could be a simple problem...
Background:
I'm trying to build a recipe app that has different recipes for each day of the week (7 days)
A json contains all the data in the hierarchy: Days -> have multiple Recipes -> have multiple Ingredients, where the ingredients have the following format:
"ingredients": [
{
"item": "cereal",
"amount": 1,
"units": "cup(s)"
},
{
"item": "milk",
"amount": 0.2,
"units": "L"
}
]
Desired Outcome:
What I'm trying to achieve is a shopping list that shows the unique list of all items that are needed for ALL recipes
How far I got so far:
The data is pulled from the json in this format which works:
struct Day: Hashable, Codable, Identifiable {
//Breakfast
var breakfastData: mealResponse
//Dinner
var dinnerData: mealResponse
struct mealResponse: Hashable, Codable {
struct IngredientResponse: Hashable, Codable {
var item: String
var amount: Double
var units: String
}
}
}
Then I manage to get the full list of ingredients like this:
ForEach(userData.days, id: \.self) { day in
ForEach(day.breakfastData.ingredients, id: \.self) {ingredient in
Text(ingredient.item)
}
}
But this is not a unique list (it just goes through every day and lists ALL ingredients for the breakfast recipes in this case...)
Would be so grateful for some help on this - have literally spent weeks trying all sorts of things...
Bonus:
The next step is then to show the sum of the "quantities" next to this unique list so the user knows how much of each ingredient s/he needs to make all the recipes in the json
To get an array of all ingredients you can use reduce and work with a Set to only add unique ingredients
let allIngredients = Array(days.reduce(into: Set<String>()) {
$0.formUnion($1.breakfastData.ingredients.map(\.item))
$0.formUnion($1.dinnerData.ingredients.map(\.item))
})
Regarding the "bonus", first of all you should only ask one question at a time but secondly let me just say that this might be much harder than expected since different recipes might use different units for the same ingredients so you will have to use a common unit between them before you can add them together. Also in your example you have a cup of cereal but that is something you buy a box of so there is another problem

How do I create a group of textfields that have separate variables that they edit in SwiftUI?

I am creating a simple program that creates several textfields to edit in. I use this code to create them:
ForEach((1...textfields).reversed(), id: \.self){ _ in
TextField("Type here:", text: $text)
}
The textfields variable is controlled by a couple of buttons earlier in the code, but the real issue is the "text" variable. The buttons add and remove textfields, but editing one edits them all, which is not what I want. So my question is, how do I create textfields that contain unique variables but can still be made from the ForEach(or similar) loop?
Question
The task here is to organise some sort of storage for the TextFields values.
Solution
Step 1
The storage can be organised using #State or #Binding modifier. As we need to have a number of TextFields here, an Array of Strings could be a good choice for storage type. This can be written like:
#State var values = ["1", "2", "3", "4", "5"]
Note that we provide here initial values as empty strings. It is for example purpose only. You can use anything that you want or replace #State with #Binding if you need to specify the values from a parent view.
Note that [String] type will be automatically inferred by swift.
Step 2
Then, the values need to be converted into TextFields in the view's body implementation. To do this, we modify your ForEach a little bit:
// iterating through the indices of the values
ForEach(values.indices, id: \.self) { index in
// using index to create binding to the value
TextField("Type here:", text: $values[index])
}
Result
And here is the final result that we have:
struct MultipleTextViews: View {
#State var values: [String] = ["1", "2", "3", "4", "5"]
var body: some View {
ForEach(values.indices, id: \.self) { index in
TextField("Type here:", text: $values[index])
}
}
}

How to create views in SwiftUI using collect

I am trying to figure out a way to insert multiple arrays of views in a VStack in SwiftUI using collect() operator.
struct ChatsTab: View {
var subscriptions = Set<AnyCancellable>()
var body: some View {
VStack {
["A", "B", "C", "D", "E"].publisher.collect(2).sink(receiveCompletion: { _ in
// Do nothing on completion
}) { (stringArray) in
HStack {
Text(stringArray[0])
Text(stringArray[1])
}
}
.store(in: &subscriptions)
}
}
}
But I'm getting this below error:
Cannot convert value of type '()' to closure result type '_'
I want to do this with collect only so I can add my text views in pair. I know I have other options but I want to get it done with collect only.
You just have to rearrange things. The real issue is not the collect, the issue is that you're trying to execute arbitrary code inside the VStack. The VStack is a function builder and between it's curly braces goes a list of views and possibly some basic if logic only. Not arbitrary code, like the kind you include in a regular function.
So if you take the publisher code out of the VStack it will compile. Where you put it is up to you, you can have it in init(), called from another function, or int didAppear view modifier (just not inside the VStack directly).
A second issue is that the collect will publish another array (one of length 2). You will essentially end up with an array of String arrays (or you can just consume them as they are published and not keep them around, depending on what you are trying to do). You can also use the simpler version of sink since the Error of the publisher here is Never. Anyway here is something incorporating the above changes:
import SwiftUI
import Combine
var subscriptions = Set<AnyCancellable>()
struct ContentView: View {
#State var stringArrays: [[String]] = []
var body: some View {
ForEach(stringArrays, id: \.self) { stringArray in
HStack {
Text(stringArray.count > 0 ? stringArray[0] : "")
Text(stringArray.count > 1 ? stringArray[1] : "")
}
}.onAppear() {
["A", "B", "C", "D", "E"].publisher
.collect(2)
.sink(receiveValue: { (stringArray) in
self.stringArrays.append(stringArray)
}).store(in: &subscriptions)
}
}
}

How to bind an array and List if the array is a member of ObservableObject?

I want to create MyViewModel which gets data from network and then updates the arrray of results. MyView should subscribe to the $model.results and show List filled with the results.
Unfortunately I get an error about "Type of expression is ambiguous without more context".
How to properly use ForEach for this case?
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var results: [String] = []
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.results = ["Hello", "World", "!!!"]
}
}
}
struct MyView: View {
#ObservedObject var model: MyViewModel
var body: some View {
VStack {
List {
ForEach($model.results) { text in
Text(text)
// ^--- Type of expression is ambiguous without more context
}
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(model: MyViewModel())
}
}
P.S. If I replace the model with #State var results: [String] all works fine, but I need have separate class MyViewModel: ObservableObject for my purposes
The fix
Change your ForEach block to
ForEach(model.results, id: \.self) { text in
Text(text)
}
Explanation
SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text) to Text(text as String) and remove the $ before model.results), is "Generic parameter 'ID' could not be inferred".
In other words, to use ForEach, the elements that you are iterating over need to be uniquely identified in one of two ways.
If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property var id: Hashable. You don't need the id parameter in this case.
The other option is to specifically tell ForEach what to use as a unique identifier using the id parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.
In this case, we chose option 2 and told ForEach to use the String element itself as the identifier (\.self). We can do this since String conforms to the Hashable protocol.
What about the $?
Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:
A Toggle needs to update a Bool value in response to a switch
A Slider needs to update a Double value in response to a slide
A TextField needs to update a String value in response to typing
The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>. So a Toggle requires you to pass it a Binding<Bool>, a Slider requires a Binding<Double>, and a TextField requires a Binding<String>.
This is where the #State property wrapper (or #Published inside of an #ObservedObject) come in. That property wrapper "wraps" the value it contains in a Binding (along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable, but if we need the binding, we can use the shorthand $myVariable.
So, in this case, your original code contained ForEach($model.results). In other words, you were telling the compiler, "Iterate over this Binding<[String]>", but Binding is not a collection you can iterate over. Removing the $ says, "Iterate over this [String]," and Array is a collection you can iterate over.