@tera can you show me how this is used in my real example? You can't just dumpProperties of a private underlying model. Compile error here
protocol Model: Identifiable where ID == String? {
var id: Self.ID { get set }
}
struct AModel: Model {
var id: String?
var name: String
}
struct BModel: Model {
var id: String?
var count:Int
}
struct AnyModel: Model {
func dumpProperties(_ model: any Model) {
// for properties accessible via existential:
print("id: \(model.id)")
// for the properties distinct to each model:
switch model {
case let model as ModelA:
print("this is modelA \(model.text)")
case let model as ModelB:
print("this is modelB \(model.count)")
default:
fatalError()
}
}
private var _model: any Model
init(_ model: some Model) {
_model = model // Automatically casts to “any” type
}
var id: String? {
get { _model.id }
set { _model.id = newValue }
}
}
struct TestView: View {
let models = [AnyModel](arrayLiteral: AnyModel(AModel(name: "test")), AnyModel(BModel(count: 3)))
var body: some View {
ForEach(models) { model in
//how to read model.name or model.count?
}
}
}
No that doesn't help the fact you are switching on the wrapper, not the underlying model. See this updated model, it crashes
protocol Model: Identifiable where ID == String? {
var id: Self.ID { get set }
}
struct AModel: Model {
var id: String?
var name: String
}
struct BModel: Model {
var id: String?
var count: Int
}
struct AnyModel: Model {
private var _model: any Model
init(_ model: some Model) {
_model = model // Automatically casts to “any” type
}
var id: String? {
get { _model.id }
set { _model.id = newValue }
}
}
struct TestView: View {
func dumpProperties(_ model: any Model) {
// for properties accessible via existential:
print("id: \(model.id)")
// for the properties distinct to each model:
switch model {
case let model as AModel:
print("this is modelA \(model.name)")
case let model as BModel:
print("this is modelB \(model.count)")
default:
fatalError()
}
}
let models = [AnyModel](arrayLiteral: AnyModel(AModel(name: "test")), AnyModel(BModel(count: 3)))
var body: some View {
ForEach(models) { anyModel in
// how to read model.name or model.count?
let _ = dumpProperties(anyModel)
}
}
}
Ok so basically you have to make the underlying model public which is typically not the design for _variables right? I guess that's the only way.
And no, your other design doesn't work because this whole topic is about getting the models to actually literally conform to identifiable. Not just exposing and id, which is what your alternative solution does. It's actually important to conform to identifiable because SwiftUI has ton of APIs which expects an identifiable where you can't just provide and id like you can in ForEach. See my example 2 in original post.
If you have a solution that doesnt use a wrapper that would be preferred as it's probably quite annoying to unwrap each time a model is accessed.
This is the final working solution I settled with, if anyone has any optimization suggestion I would love to hear it.
protocol Model: Identifiable where ID == String? {
var id: Self.ID { get set }
}
struct AModel: Model {
var id: String?
var name: String
}
struct BModel: Model {
var id: String?
var count: Int
}
struct AnyModel: Model {
var innerModel: any Model
init(_ model: some Model) {
innerModel = model // Automatically casts to “any” type
}
var id: String? {
get { innerModel.id }
set { innerModel.id = newValue }
}
}
struct TestView: View {
@State var sheetData: AnyModel?
let models = [AnyModel](arrayLiteral: AnyModel(AModel(id: "1", name: "test")), AnyModel(BModel(id: "2", count: 3)))
var body: some View {
VStack {
ForEach(models) { anyModel in
switch anyModel.innerModel {
case let a as AModel:
Text("\(a.name)")
case let b as BModel:
Text("\(b.count)")
default:
Text("ERROR")
}
}
}
.sheet(item: $sheetData) { _ in
Text("hi")
}
}
}
import SwiftUI
protocol Model: Identifiable {
var id: String? { get set }
var view: any View { get }
}
struct AModel: Model {
var id: String?
var name: String
var view: any View {
AView(model: self)
}
}
struct BModel: Model {
var id: String?
var count: Int
var view: any View {
BView(model: self)
}
}
struct AView: View {
let model: AModel
var body: some View {
Text("\(model.name)")
}
}
struct BView: View {
let model: BModel
var body: some View {
Text("\(model.count)")
}
}
struct AnyModel: Model {
var innerModel: any Model
init(_ model: any Model) {
innerModel = model
}
var id: String? {
get { innerModel.id }
set { innerModel.id = newValue }
}
var view: any View {
innerModel.view
}
}
struct TestView: View {
@State var sheetData: AnyModel?
let models: [AnyModel] = [AnyModel(AModel(id: "1", name: "test")), AnyModel(BModel(id: "2", count: 3))]
var body: some View {
VStack {
ForEach(models) { model in
AnyView(model.view)
}
}
.sheet(item: $sheetData) { _ in
Text("hi")
}
}
}
struct ContentView: View {
var body: some View {
TestView()
}
}
@main struct TheApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
Using a wrapper AnyModel for Identifiable conformance
getting rid of switch
separate views for separate models
models providing relevant views
using type erased AnyView
I'd also check how this works with mutations (unless your models don't change): I recon there'd need to be @ObservableObject / @ObservedObject / @Published or their analogues from the new observation machinery.
ok I started refactoring and it seems I ran into a roadblock when I try to use binding.
to extend from your example
class VM:ObservableObject {
@Published var vmModel:AnyModel = AnyModel(AModel(id: "1", name: "test"))
}
struct TestView: View {
@State var sheetData: AnyModel?
@StateObject var vm = VM()
let models: [AnyModel] = [AnyModel(AModel(id: "1", name: "test")), AnyModel(BModel(id: "2", count: 3))]
var body: some View {
VStack {
ForEach(models) { model in
AnyView(model.view)
}
TextField(text: ???) //how can I get a binding of AModel?
}
.sheet(item: $sheetData) { _ in
Text("hi")
}
}
}
TextField("TextField", text: .init(get: {
let m = vm.vmModel.innerModel as! AModel
return m.name
}, set: { newValue in
var m = vm.vmModel.innerModel as! AModel
m.name = newValue
vm.vmModel.innerModel = m
}))
A few of these and it'd start feeling you are fighting the system.
I might add that you can use dynamic member lookup to access your wrapped model's properties without having to switch over them every time:
@dynamicMemberLookup
enum WidgetModel: Identifiable {
case a(A)
case b(B)
…
subscript<T>(dynamicMember keyPath: KeyPath<any Model, T>) -> T {
switch self {
case .a(let a): a[keyPath: keyPath]
case .b(let b): b[keyPath: keyPath]
...
}
}
so you can do:
struct ContentView: View {
let models = [WidgetModel]()
var body: some View {
ForEach(models) { model in
model.name // assuming the Model protocol defines a 'name' property
}
}
}