Home > Enterprise >  Edit different structs/models in the same View in SwiftUI
Edit different structs/models in the same View in SwiftUI

Time:12-15

I didn't find any posts/ways to load/manage different objects in the same view in SwiftUI. The goal is to not multiply same code.

Supposing I have 3 structs/instances Country, Region, Subregion with same properties: id, name, image.

struct Country: Identifiable, Codable, Hashable {       
  var id: Int64?
  var name: String
  var image: String   
}

struct Region: Identifiable, Codable, Hashable {        
  var id: Int64?
  var name: String
  var image: String   
}

struct Subregion: Identifiable, Codable, Hashable {     
  var id: Int64?
  var name: String
  var image: String   
}

In a a parent view, I want to send instances to the child view as a binding:

ButtonNavigationLink(descriptorItem: descriptorItem, selectedItem: $finderViewModel.referenceCountry)
ButtonNavigationLink(descriptorItem: descriptorItem, selectedItem: $finderViewModel.referenceRegion)
ButtonNavigationLink(descriptorItem: descriptorItem, selectedItem: $finderViewModel.referenceSubregion)

Note: I don't want to send the same properties to the child view, this is easy. I want to send the struct itself (model). I need it in the child view.

So now, I want to receive the model/struct in the child view. I have done multiple try such:

  • using generics: @Binding var selectedItem: GenericType? => the problem is I don't know how to read the properties from the generic (cast ?)
  • trying AnyHashable: @Binding var selectedItem: AnyHashable? => the compiler is lost between the type sent (country) and AnyHashable type

Well, I am lost. :-)

My subview that receive the "generic" binding looks like that:

// struct ButtonNavigationLink <T>: View where T: Hashable { // => if generic
struct ButtonNavigationLink: View {
        
    var descriptorItem: DescriptorItem
    
    @Binding var selectedItem: AnyHashable?

    // Generic type
    //@Binding var selectedItem: GenericType?   
    //typealias GenericType = T
}

So what's the best solution to not multiply views for each object ? The child view goal is to select the item in the @Binding, not only to display data... that's why I need to full object and not only the properties as @Binding.

Button (action: {
  selectedItem = descriptorItem
 }) 

Thanks in advance for any help/suggestion.

CodePudding user response:

The most obvious way to do this is to define a protocol:

protocol Item {
    var id: Int64? { get }
    var name: String { get }
    var image: String { get }
}

Then make your structs conform to that protocol:

struct Country: Identifiable, Codable, Hashable, Item {
struct Region: Identifiable, Codable, Hashable, Item {
struct Subregion: Identifiable, Codable, Hashable, Item {

And finally, change your binding to use the protocol type:

struct DetailsView: View {

    @Binding var selectedItem: Item?

    var body: some View {
        VStack {
            if let selectedItem {
                if selectedItem is Region { // <- If you want to check which type was passed
                    Text("Region name: \(selectedItem.name)")
                }
            }
        }
    }
}


struct ContentView: View {

    @State var selectedItem: Item?

    var body: some View {

        VStack {
            Button("Set to Region") {
                selectedItem = Region(id: 1, name: "region", image: "Image")
            }

            Button("Set to Country") {
                selectedItem = Country(id: 0, name: "contry", image: "Image")
            }

            NavigationStack {
                NavigationLink("Pass \(selectedItem?.name ?? "nil")", destination: DetailsView(selectedItem: $selectedItem))
            }
        }
    }
}

CodePudding user response:

Instead of Hashable you can make your own "protocol" and then use any notice the lowercase any, uppercase AnyHashable is very different

import SwiftUI
protocol BasicProtocol: Codable, Hashable, Identifiable{
    var id: Int64? {get set}
    var name: String {get set}
    var image: String {get set}
    static var sample: Self {get}
}
struct Country: BasicProtocol {
    var id: Int64?
    var name: String
    var image: String
    
    static let sample: Self = .init(id: 1, name: "United States", image: "figure.skiing.crosscountry")
}

struct Region: BasicProtocol {
    var id: Int64?
    var name: String
    var image: String
    static let sample: Self = Region(id: 2, name: "West", image: "r.square")
}

struct Subregion: BasicProtocol {
    var id: Int64?
    var name: String
    var image: String
    static let sample: Self = .init(id: 3, name: "California", image: "opticaldiscdrive")
}

struct SomeOtherModel{
    let country: Country
    let region: Region
    let subRegion: Subregion
    static let sample: Self = .init(country: .sample, region: .sample, subRegion: .sample)
}

The variable that holds the selected item would then become an "existential".

var selectedItem: (any BasicProtocol)?

So your Button View can assign the object.

struct SomeOtherModelView: View {
    @State var selectedScope: (any BasicProtocol)?
    let someModel: SomeOtherModel = .sample
    var body: some View{
        VStack{
            if let selectedItem = selectedScope{
                Text("You have selected\n**\(selectedItem.name)**")
                    .multilineTextAlignment(.center)
            }else{
                Text("Please select an item\n")
            }
            GenericButtonView(selectedItem: $selectedScope, item: someModel.country)
            GenericButtonView(selectedItem: $selectedScope, item: someModel.region)
            GenericButtonView(selectedItem: $selectedScope, item: someModel.subRegion)
        }
    }
}

struct GenericButtonView: View {
    @Binding var selectedItem: (any BasicProtocol)?
    let item: any BasicProtocol
    var body: some View {
        Button {
            selectedItem = item
        } label: {
            Label(item.name, systemImage: item.image)
        }
        
    }
}
struct GenericButtonSampleView_Previews: PreviewProvider {
    static var previews: some View {
        SomeOtherModelView()
    }
}

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

https://docs.swift.org/swift-book/LanguageGuide/Generics.html

https://www.avanderlee.com/swift/existential-any/

You can also combine the different types into an Array<any BasicProtocol> there is sample here

  • Related