0

I have the following code:

struct Car: Hashable, Identifiable, Codable {
    var id = UUID()
    var year:Int
    var make:String
    var model:String
    var notes:String?
    
    static var testCars:[Car] {
        return [
            Car(year: Int.random(in: 1990...2023), make: "Honda", model: "S800"),
            Car(year: Int.random(in: 1990...2023), make: "Honda", model: "Civic"),
        ]
    }
}



enum CarViewRouter:Hashable, Identifiable, Codable {
    
    static func == (lhs: CarViewRouter, rhs: CarViewRouter) -> Bool {
        
        var lhsCarId:UUID
        switch lhs {
        case .car(let car):
            lhsCarId = car.id
        }
        
        var rhsCarId:UUID
        switch rhs {
        case .car(let car):
            rhsCarId = car.id
        }
        
        return lhsCarId == rhsCarId
        
    }
        
    var id: UUID {
        switch self {
        case .car(let car):
            return car.id
        }
    }
    
    case car(Binding<Car>)
}



struct ContentView: View {
    
    @State var cars = [Car]()
    @State var path = NavigationPath()
    
    var body: some View {
        
        NavigationStack {
            VStack {
                ForEach($cars) { $car in
                    Button {
                        path.append(CarViewRouter.car($car))
                    } label: {
                        Text(car.model)
                    }
                }
            }
            .task {
                cars = Car.testCars
            }
            .navigationDestination(for: CarViewRouter.self) { d in
                switch d {
                case .car(let car):
                    CarDetails(car: car)
                }
            }
        }
        
    }
    
}



struct CarDetails:View {
    
    @Binding var car:Car
    
    var body: some View {
    
        Text("\(car.make) - \(car.model) - \(car.year)")
        
    }
    
    
}

I am unable to make it compile. Errors:

Type 'CarViewRouter' does not conform to protocol 'Decodable'

Type 'CarViewRouter' does not conform to protocol 'Encodable'

Type 'CarViewRouter' does not conform to protocol 'Hashable'

How can I make the CarViewRouter as defined conform to those protocols so that it compiles?

[added the entire code to provide complete context. I was trying to pass in a Car so that a subview could eventually edit it...]

zumzum
  • 17,984
  • 26
  • 111
  • 172
  • 1
    `Binding` does not conform to Codable and besides it makes no sense to en-/decode property wrappers like `@State` and `@Binding`. – vadian Jul 24 '23 at 14:52
  • You have to write it out, but this won't work. I know what you are trying to do, it will be unstable, I posted a solution with this approach a while ago. – lorem ipsum Jul 24 '23 at 14:53
  • Just switch to reference types, it is what the documentation recommends for Data Models. I stoped using `struct` for data models with the last WWDC, value types are for UI State not Data Models. – lorem ipsum Jul 24 '23 at 14:54
  • https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app – lorem ipsum Jul 24 '23 at 14:56
  • @loremipsum "value types are for UI State not Data Models" that's quite a controversial take. Or maybe your understanding of what a Data Model is different than mine. `Codable` objects should always be immutable structs - these are [DTO](https://stackoverflow.com/questions/1051182/what-is-a-data-transfer-object-dto)s though, not state objects. It's best practice to use different models for talking with backend services and for representing app state. – Dávid Pásztor Jul 24 '23 at 14:57
  • @DávidPásztor it is what apple recommends https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app , it isn't my take. – lorem ipsum Jul 24 '23 at 14:58
  • @zumzum what is the actual problem you are trying to solve? Why do you think you need a `Codable` object to store a `Binding`? – Dávid Pásztor Jul 24 '23 at 14:58
  • I see. I am trying to pass down a `Car` so that a subview can edit it... Looks like I am thinking about it wrong. I will check this link out to see what I need to do instead. – zumzum Jul 24 '23 at 14:58
  • @loremipsum that doc is talking about mutable state objects for driving the UI, not codable objects used for communicating with backend services. – Dávid Pásztor Jul 24 '23 at 14:59
  • @loremipsum In my opinion the "Discover Observable" video is pretty misleading. `Observable` replaces `ObservableObject`, the **View Model** but it's still recommended to use structs for the **Data Model**. – vadian Jul 24 '23 at 15:00
  • @zumzum you should decouple your `Codable` objects from your view models. Create the mutable objects from your models and only convert them back to model objects when you want to use them outside your app (whether that's for sending over the network or persisting on disk). – Dávid Pásztor Jul 24 '23 at 15:00
  • @DávidPásztor it is talking about "Managing model data" like a `Book` – lorem ipsum Jul 24 '23 at 15:00
  • Lol, this is a can of worms, I won't convince any of y'all and y'all won't convince me, I can argue both ways. What I do know is that SwiftUI's `navigationDestination` sucks with `Binding` every release there is a new issue of people talking about skipping textfields, getting sent back to the beginning, etc. It just doesn't work and until Apple "fixes" that I think reference types are more versatile. CoreData objects are reference types, MongoDB uses references types, it is a choice. – lorem ipsum Jul 24 '23 at 15:04
  • I Appreciate and am reading all comments here. Trying to figure out proper way forward. I have to say I am not too clear. Given an array of structs, I would like to allow subviews to edit them as needed and I thought I could do it with a binding. I guess I have read the links posted here and understand I this should be done. Very confused at the moment. – zumzum Jul 24 '23 at 15:06
  • @zumzum there is no "proper" way there are style choices but the only way to viably use `Binding` with `NavigationStack` is using `NavigationLink` that explicitly provides the `destination` using your current method you will have issues anytime the `Binding` is used. Your text fields will skip, your views will pop and sometimes you won't see updates because only the `wrappedValue` is changing. These bugs are different by the iOS version sometimes it works sometimes it doesn't. There are endless questions on SO about the topic. – lorem ipsum Jul 24 '23 at 15:11
  • @vadian I am not talking about the video I am talking about the document that Apple has edited (linked above). Before it was with `ObservableObject`s now it has `Observable`. There is another document I found from apple that explicitly talks about choosing, of course I can't find it now but from what I remember it said if you interface with any C type code you should use reference types (I do). I think `Binding` via navigation and the limitation with `any` is worth the change. Reference types and inheritance have their advantages. – lorem ipsum Jul 24 '23 at 15:19
  • I am going through the link posted https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app and it's very helpful. I found all these comments very insightful. So, thank you! – zumzum Jul 24 '23 at 15:27
  • @loremipsum I agree with *if you interface with any C type code you should use reference types*. There is a link in the article leading to [`Choosing Between Structures and Classes`](https://developer.apple.com/documentation/Swift/choosing-between-structures-and-classes) which says *Use structures by default*. – vadian Jul 24 '23 at 15:27
  • Does this answer your question? [Why are my destination links only binding the last contact as the destination link's data?](https://stackoverflow.com/questions/74973092/why-are-my-destination-links-only-binding-the-last-contact-as-the-destination-li) – lorem ipsum Jul 24 '23 at 15:28
  • Here are some questions on the subject [1](https://stackoverflow.com/questions/75227263/swiftui-pass-binding-through-navigationstack-enum/75227489#75227489) [2](https://stackoverflow.com/questions/75216625/how-to-stop-text-cursor-from-jumping-to-the-end-in-swiftui/75216851#75216851) – lorem ipsum Jul 24 '23 at 15:28
  • I'll check that question out now. Good stuff. – zumzum Jul 24 '23 at 15:29
  • @vadian I agree by that be default, I have just exhausted using it with SwiftUI data models, I believe that for UI management `struct` is best, I have tried since 2019 when I was at WWDC and was so into the whole framework, it does not work, `Observable` seals the deal for me. – lorem ipsum Jul 24 '23 at 15:29
  • @zumzum the question I used for duplicate is I think the best option if you must stick with `struct` – lorem ipsum Jul 24 '23 at 15:33
  • 1
    @loremipsum very helpful. I don't have to stick to structs on my end really. I am trying to understand "best" way forward to build something clean and stable. It is challenging still to just know the pattern to adopt overall (for me at least) but I am reading all the material provided here and will try to find a good way forward. I will post some feedback as to what works for me... thank you!! – zumzum Jul 24 '23 at 15:37
  • just asking what proper protocol is here. I modified the code to use reference type and I wanted to post what I changed things to. Should I just close this question or post what is working for me now? not sure. – zumzum Jul 24 '23 at 17:02
  • @zumzum you can leave it, another might benefit from it it is up to you. It seems like a good example of the approach. – lorem ipsum Jul 24 '23 at 19:20

1 Answers1

0

I ended up switching to reference type, kept navigation path router enum approach, and used a navigation link with the router. Seems to be working fine.

@main
struct architecturesApp: App {
    
    @State private var dealership = Dealership()
    
    var body: some Scene {
        WindowGroup {
            DealershipView()
                .environment(dealership)
        }
    }
}




@Observable final class Car: Identifiable {
    
    var id = UUID()
    var year:Int
    var make:String
    var model:String
    
    var hashValue: Int {
        return id.hashValue
    }
    
    init(year: Int, make: String, model: String) {
        self.year = year
        self.make = make
        self.model = model
    }
    
}

@Observable final class Dealership {
    
    var cars:[Car] = [
        Car(year: 1, make: "make 1", model: "model 1" ),
        Car(year: 2, make: "make 2", model: "model 2" ),
        Car(year: 3, make: "make 3", model: "model 3" )
    ]
    
}

enum CarViewsRouter: Hashable, Equatable, Identifiable {
    
    var id: ObjectIdentifier {
        switch self {
        case .car(let c):
            return c.id
        }
    }
    
    static func == (lhs: CarViewsRouter, rhs: CarViewsRouter) -> Bool {
        lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        switch self {
        case .car(let c):
            hasher.combine(c.id)
        }
    }
    
    case car(Car)
}


struct DealershipView: View {
    
    @Environment(Dealership.self) private var dealership
    @State var navPath = NavigationPath()

    var body: some View {
        NavigationStack {
            List(dealership.cars) { car in
                NavigationLink(value: CarViewsRouter.car(car)) {
                    Text("\(car.model). YEAR: \(car.year)")
                }
            }
            .navigationDestination(for: CarViewsRouter.self) { d in
                switch d {
                case .car(let car):
                    CarView(car: car)
                }
            }
        }
        
    }
    
}


struct CarView: View {
    
    @Bindable var car:Car
    
    var body: some View {
        Button(action: {
            car.year = Int.random(in: 1...100)
        }, label: {
            Text("CAR: \(car.model). Year: \(car.year)")
        })
    }
    
}
zumzum
  • 17,984
  • 26
  • 111
  • 172