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