Cannot convert value of type 'Binding<_>' to expected argument type 'Binding<Card>' - ios

I am trying to create a binding to a FetchedResults item, error is on $items[i]:
struct NavView: View {
#Binding var item : Card
...
}
struct ContentView: View {
private var items: FetchedResults<Card>
var body: some View {
List {
ForEach(items.indices, id:\.self) { i in
NavigationLink {
NavView(item: $items[i])
}
}
}
}
}

Changing the Binding to ObservedObject compiles and seems to work properly, although I feel like I'm violating single source of truth policy by creating a new ObservedObject.
struct NavView: View {
#ObservedObject var item : Card
...
}
struct ContentView: View {
private var items: FetchedResults<Card>
var body: some View {
List {
ForEach(items) { item in
NavigationLink {
NavView(item: item)
}
}
}
}
}

Related

Infinite loop by using #Binding when passing data between views

High-level description:
There is a nested view problem when a state object is being passed through views. At the end of the deepest view in the hierarchy, the app is frozen and memory consumption is increasing continuously.
Use-case
Partners list → Partner detail → (Locations list) → Location detail
Code-snippets
class PartnerViewModel: ObservableObject {
#Published var partners: [Partner] = Partner.partners
}
This view is loaded into a TabView and a NavigationStack components in the parent class.
struct PartnerListView: View {
#StateObject var viewModel = PartnerViewModel()
var body: some View {
List($viewModel.partners, id: \.self) { $partner in
NavigationLink {
PartnerDetailView(partner: $partner)
} label: {
Text(partner.name)
}
}
}
}
struct PartnerDetailView: View {
#Binding var partner: Partner
var body: some View {
Form {
Section("Locations") {
List($partner.locations, id: \.self) { $location in
NavigationLink {
LocationDetailView(location: $location)
} label: {
Text(location.name)
}
}
}
}
}
}
struct LocationDetailView: View {
#Binding var location: Location
var body: some View {
TextField("Name", text: $location.name)
}
}
The following snippets are workaround and it works but it might be temporary because I don't understand why the first attempt doesn't work and why this one does. I haven't found any resources that could give an example of this scenario.
struct PartnerDetailView: View {
#Binding var partner: Partner
var body: some View {
Form {
Section("Locations") {
List($partner.locations, id: \.self) { $location in
NavigationLink {
LocationDetailView(partner: $partner, locationIndex: partner.locations.firstIndex(of: location) ?? 0)
} label: {
Text(location.name)
}
}
}
}
}
}
struct LocationDetailView: View {
#Binding var partner: Partner
var locationIndex: Int
var body: some View {
TextField("Name", text: $partner.locations[locationIndex].name)
}
}
Is it possible that I am not passing values between views properly?🤔

Updating a binding value pops back to the parent view in the navigation stack

I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}

SwiftUI MVVM Binding List Item

I am trying to create a list view and a detailed screen like this:
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
LazyVStack {
// https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
ForEach(viewModel.items.identifiableIndicies) { index in
MyListItemView($viewModel.items[index])
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = []
...
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item), label: {
...
})
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
#StateObject var viewModel: MyListViewItemDetailModel
init(item: Binding<Item>) {
viewModel = MyListViewItemDetailModel(item: item)
}
var body: some View {
...
}
}
class MyListViewItemDetailModel: ObservableObject {
var item: Binding<Item>
...
}
I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel.
Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?
I think you should think about a minor restructure of your code, and use only 1
#StateObject/ObservableObject. Here is a cut down version of your code using
only one StateObject source of truth:
Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.
PS: what is identifiableIndicies?
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Item: Identifiable {
let id = UUID().uuidString
var name: String = ""
}
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
var body: some View {
LazyVStack {
ForEach(viewModel.items.indices) { index in
MyListItemView(item: $viewModel.items[index])
}
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item)){
Text(item.name)
}
}
}
class MyAPIModel {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your fetching here
completion(Item(name: "new data from api"))
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
let myApiModel = MyAPIModel()
var body: some View {
VStack {
Button(action: fetchNewData) {
Text("Fetch new data")
}
TextField("edit item", text: $item.name).border(.red).padding()
}
}
func fetchNewData() {
myApiModel.fetchItemData() { itemData in
item = itemData
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyListView()
}.navigationViewStyle(.stack)
}
}
EDIT1:
to setup an API to call some functions, you could use something like this:
class MyAPI {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your stuff
}
}
and use it to obtain whatever data you require from the server.
EDIT2: added some code to demonstrate the use of an API.

SwiftUI is it possible to pass range of binding in ForEach?

I'd like to pass a range of an array in a model inside ForEach.
I recreated an example:
import SwiftUI
class TheModel: ObservableObject {
#Published var list: [Int] = [1,2,3,4,5,6,7,8,9,10]
}
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { item in
SubView(subList: $model.list[0..<5]) <-- error if I put a range
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var body: some View {
HStack {
ForEach (subList, id:\.self) { item in
Text("\(item)")
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
The work around
I found is to pass all the list and perform the range inside the subView. But I'd like don't do this because the array is very big:
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { i in
SubView(subList: $model.list, number: i, dimension: 5)
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var number: Int
var dimension: Int
var body: some View {
HStack {
ForEach (subList[number*dimension..<dimension*(number+1)].indices, id:\.self) { idx in
Button(action: {
subList[idx] += 1
print(subList)
}, label: {
Text("num: \(subList[idx])")
})
}
}
}
}
I would pass the model to the subview since it is a class and will be passed by reference and then pass the range as a separate parameter.
Here is my new implementation of SubView
struct SubView: View {
var model: TheModel
var range: Range<Int>
var body: some View {
HStack {
ForEach (model.list[range].indices, id:\.self) { idx in
HStack {
Button(action: {
model.list[idx] += 1
print(model.list)
}, label: {
Text("num: \(model.list[idx])")
})
}
}
}
}
}
Note that I added indices to the ForEach header to make sure we access the array using an index and not with a value from the array.
The calling view would then look like
var body: some View {
VStack {
SubView(model: model, range: (0..<5))
Text("\(model.list.map(String.init).joined(separator: "-"))")
}
The extra Text is just there for testing purposes

Binding an Array through multiple Views in SwiftUI

I'm stuck with this problem. I've the following struct that builds a List from a #Binding Array
struct AppleList: View {
#Binding var apples: [Apple]
var body: some View {
NavigationView {
List {
ForEach(apples) { apple in
NavigationLink(destination: AppleDetail(apple: $apple)) {
AppleRow(apple: apple)
}
}
}
}
}
}
AppleDetail has an edit button that switches between AppleSummary and AppleEditor, so here the apple will be modified.
struct AppleDetail: View {
#Binding var apple: Apple
var body: some View {
...
}
}
AppleRow doesn't need to modify apple.
struct AppleRow: View {
var apple: Apple
var body: some View {
...
}
}
The problem is in the ForEach loop: how can make a binding element from a binding array of elements that when they will be modified will send the modification to the parent of AppleList?
Solution was quite simple:
ForEach(apples.indices) { idx in
NavigationLink(destination: AppleDetail(transaction: self.$apples[idx])) {
AppleRow(transaction: apples[idx])
}
}

Resources