SwiftUI iterating through #State or #Published dictionary with ForEach - swift

Here is a minimum reproducible code of my problem. I have a dictionary of categories and against each category I have different item array. I want to pass the item array from dictionary, as binding to ListRow so that I can observer the change in my ContentView. Xcode shows me this error which is very clear
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Item' conform to 'Identifiable.
The solution shows in this question Iterating through set with ForEach in SwiftUI not using any #State or #Published variable. They are just using it for showing the data. Any work around for this issue ??
struct Item {
var id = UUID().uuidString
var name: String
}
struct ListRow {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
ForEach(testDictionary[category]) { item in
ListRow(item: item)
}
}
}.onAppear(
addDummyDateIntoDictonary()
)
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}

One problem is that you didn't make ListRow conform to View.
// add this
// ╭─┴──╮
struct ListRow: View {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
Now let's address your main problem.
A Binding is two-way: SwiftUI can use it to get a value, and SwiftUI can use it to modify a value. In your case, you need a Binding that updates an Item stored somewhere in testDictionary.
You can create such a Binding “by hand” using Binding.init(get:set:) inside the inner ForEach.
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
let items = testDictionary[category] ?? []
ForEach(items, id: \.id) { item in
let itemBinding = Binding<Item>(
get: { item },
set: {
if
let items = testDictionary[category],
let i = items.firstIndex(where: { $0.id == item.id })
{
testDictionary[category]?[i] = $0
}
}
)
ListRow(item: itemBinding)
}
}
}.onAppear {
addDummyDateIntoDictonary()
}
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}

Related

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

Load different data by ID in the same View

I want to load different data in the same View using an ObservableObject with an Index.
As you can see in the simple demo example attached when I go through the views changing the id in order to load different data it works.
But when in any point I change another data from another observable object (in this case AnotherObservable) then my view breaks and any data is found at all.
Navigation works...
Once I press Change Title button...
To test de issue just simple copy and paste the code and navigate through the data with the <- -> and then press Change title button
class DemoItemsLoader: ObservableObject {
#Published var items = [1, 2, 3, 4]
}
class DemoArticleLoader: ObservableObject {
#Published var items = [1 : "One", 2 : "Two" , 3 : "Three", 4 : "Four"]
#Published var realData: String?
func loadItemForID(id: Int) {
realData = items[id] ?? "Not found"
}
}
class AnotherObservable: ObservableObject {
#Published var title: String = "Title"
}
struct Class1: View {
#StateObject var anotherObservable = AnotherObservable()
#StateObject var itemsLoader = DemoItemsLoader()
var body: some View {
NavigationView{
VStack {
ForEach(itemsLoader.items, id: \.self) { item in
NavigationLink(
destination: Class2(anotherObservable: anotherObservable, articleLoader: DemoArticleLoader(), id: item)) {
Text("\(item)")
}
}
}
}
}
}
struct Class2: View {
#ObservedObject var anotherObservable: AnotherObservable
#ObservedObject var articleLoader: DemoArticleLoader
#State var id: Int
var body: some View {
VStack {
Text(anotherObservable.title)
Text(articleLoader.realData ?? "Not found")
HStack {
Spacer()
Button(action: {
if id > 1 {
id = id - 1
articleLoader.loadItemForID(id: id)
}
}) {
Text("<-")
}
Button(action: {
if id < articleLoader.items.count {
id = id + 1
articleLoader.loadItemForID(id: id)
}
}) {
Text("->")
}
Button(action: {
anotherObservable.title = "Another Title"
}) {
Text("Change title")
}
Spacer()
}
}
.onAppear { articleLoader.loadItemForID(id: id)}
}
}
The issue is that you're calling loadItemForID in onAppear only:
.onAppear { articleLoader.loadItemForID(id: id)}
And when you change title, the above function is not called again.
The easiest solution is probably to store DemoArticleLoader() as a #StateObject, so it's not recreated every time:
struct Class1: View {
#StateObject var anotherObservable = AnotherObservable()
#StateObject var itemsLoader = DemoItemsLoader()
#StateObject var articleLoader = DemoArticleLoader() // create once
var body: some View {
NavigationView {
VStack {
ForEach(itemsLoader.items, id: \.self) { item in
NavigationLink(
// pass here
destination: Class2(anotherObservable: anotherObservable, articleLoader: articleLoader, id: item)) {
Text("\(item)")
}
}
}
}
}
}

SwiftUI SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}

SwiftUI updating array state doesn't update view

Here is my test code:
struct ArrView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(arr.indices) {row in
Text(self.arr[row])
}
}
}
}
When the 'append' Button was pressed, the List view didn't update.
Am I doing wrong?
You can use ForEach<Range>() only if the provided data is a constant, otherwise you need to use ForEach(_:id:content:) and provide an id explicitly, like this:
struct ArrView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(arr, id: \.self) { item in
Text(item)
}
}
}
}
I don't think it is the proper way to create List view. Here is a sample
struct ContentView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(self.arr, id: \.self) { str in
Text(str)
}
}
}
}

List selection as Set<String> - how to use?

Am playing around with SwiftUI and am obviously not getting it.
Basic example which works and is just displaying the selected name.
struct ContentView: View {
let names = ["Joe", "Jim", "Paul"]
#State var selectedName = Set<String>()
var body: some View {
VStack {
List(names, id: \.self, selection: $selectedName) { name in
Text(name)
}
if !selectedName.isEmpty {
Text(selectedName.first!) // <-- this line
}
}
}
}
What I want is a textfield where that name can be changed. Tried many ways but getting another error every time.
TextField("Name", text: $selectedName)
Gives this error: Cannot convert value of type 'Binding<Set<String>>' to expected argument type 'Binding<String>'
TextField("Name", text: $selectedName.first!)
Cannot force unwrap value of non-optional type 'Binding<((String) throws -> Bool) throws -> String?>'
How would I do this?
You may make a binding by yourself:
TextField("Name", text: Binding<String>(get: {self.selectedName.first!}, set: { _ in}) )
Obviously you can't pass Binding<Set<String>> to Binding<String>. Here gives you an idea or solution to change selectedName variable using TextField:
I added a new variable which is Binding<String>. Then I change the selectedName inside the TextField's onCommit closure.
struct ContentView: View {
let names = ["Joe", "Jim", "Paul"]
#State var selectedName = Set<String>()
#State var textFieldName = ""
var body: some View {
VStack {
List(names, id: \.self, selection: $selectedName) { name in
Text(name)
}
if !selectedName.isEmpty {
Text(selectedName.first!)
}
Text(textFieldName)
TextField("Name", text: $textFieldName, onEditingChanged: { (Bool) in
//onEditing
}) {
//onCommit
self.selectedName.insert(self.textFieldName)
}
}
}
}
Ok, here is my alternate if I'd needed to edit some value of names having in one screen and list and edit field and make them all synchronised and not confuse each other.
Here is full testable module (tested on Xcode 11.2/iOS 13.2). As I tested it for iOS there are API requirement for put List into EditMode to process selection, so this included.
struct TestChangeSelectedItem: View {
#State var names = ["Joe", "Jim", "Paul"] // made modifiable
#State var selectedName: String? = nil // only one can be edited, so single selection
#State var editMode: EditMode = .active // Tested for iOS, so it is needed
var body: some View {
VStack {
List(selection: $selectedName) {
ForEach(names, id: \.self) { name in
Text(name)
}
}
.environment(\.editMode, $editMode) // Tested for iOS, so it is needed
if selectedName != nil {
Divider()
Text(selectedName!) // Left to see updates for selection
editor(for: selectedName!) // Separated to make more clear
}
}
}
private func editor(for selection: String) -> some View {
let index = names.firstIndex(of: selection)!
var editedValue = selection // local to avoid cycling in refresh
return HStack {
Text("New name:")
TextField("Name", text: Binding<String>(get: { editedValue }, set: { editedValue = $0}), onCommit: {
self.names[index] = editedValue
self.selectedName = editedValue
})
}
}
}
struct TestChangeSelectedItem_Previews: PreviewProvider {
static var previews: some View {
TestChangeSelectedItem()
}
}