iOS 응용 (SwiftUI)

[iOS - SwiftUI] ObservableObject 모델을 wrapping하여 protocol 다루기 ("Type any cannot conform to ObservableObject", @ObservedObject, ObservableObject)

jake-kim 2024. 8. 30. 01:59

@ObservedObject 사용 요건

  • ObservableObject 모델을 사용할 때 아래 에러를 마주치는 경우가 존재

  • 바로 @ObservedObject를 선언한 프로퍼티는 프로토콜이 아닌 구체적인 타입을 사용해야함
struct ContentView: View {
    // error: Type 'any Personable' cannot conform to 'ObservableObject'
    @ObservedObject var person: Personable 
    
    var body: some View {
        HStack {
        }
    }
}

protocol Personable where Self: ObservableObject {
    var name: String { get }
    var age: Int { get }
}

class Person: Personable, ObservableObject {
    @Published var name: String
    @Published var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

 

  • any를 붙여도 또 다른 에러가 발생
// error: Type 'any Personable' cannot conform to 'ObservableObject'
@ObservedObject var person: any Personable

해결 방법

  • Personable을 내부적으로 가지고 있는 Wrapper class를 만들기
class AnyPerson: ObservableObject {
    @Published var wrapped: any Personable
    
    init(_ wrapped: any Personable) {
        self.wrapped = wrapped
    }
}
  • 이렇게 만들면 @ObservedObject를 선언한 쪽에서도 구체적인 타입을 사용하기 때문에 오류가 나지 않음
struct ContentView: View {
    @ObservedObject var person: AnyPerson // OK!
    
    var body: some View {
        HStack {
        }
    }
}

KeyPath를 통해 더욱 편리하게 만들기

  • 모델이 아래와 같이 생기면 이 모델을 사용할 때 불필요하게 wrapped에 직접 접근해서 name, age 속성에 접근해야함
class AnyPerson: ObservableObject {
    @Published var wrapped: any Personable
    
    init(_ wrapped: any Personable) {
        self.wrapped = wrapped
    }
}

@ObservedObject var person: AnyPerson

...

person.wrapped.name
person.wrapped.age
  • wrapped 없이 person.name, person.age 로 접근할 수 없을까?
    • keyPath를 적용하면 wrapped 접근을 생략할 수 있고, wrapped도 private으로 만들어서 불필요한 정보를 외부에 노출 방지가 가능
    • getter 역할을 하는 KeyPath와 setter 역할을 하는 WritableKeyPath 선언
@dynamicMemberLookup
class AnyPerson: ObservableObject {
    @Published private var wrapped: any Personable
    
    init(_ wrapped: any Personable) {
        self.wrapped = wrapped
    }
    
    subscript<T>(dynamicMember keyPath: KeyPath<any Personable, T>) -> T {
        wrapped[keyPath: keyPath]
    }
    
    subscript<T>(dynamicMember keyPath: WritableKeyPath<any Personable, T>) -> T {
        get { wrapped[keyPath: keyPath] }
        set { wrapped[keyPath: keyPath] = newValue }
    }
}

완성)

struct ContentView: View {
	@ObservedObject var person: AnyPerson
    
    var body: some View {
        HStack {
            
        }
        .onAppear {
            print(person.age) // OK!
            print(person.name) // OK!
        }
    }
}

 

--- 전체 코드

struct ContentView: View {
    @ObservedObject var person: AnyPerson
    
    var body: some View {
        HStack {
            
        }
        .onAppear {
            print(person.age)
            print(person.name)
        }
    }
}

protocol Personable where Self: ObservableObject {
    var name: String { get }
    var age: Int { get }
}

class Person: Personable, ObservableObject {
    @Published var name: String
    @Published var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

@dynamicMemberLookup
class AnyPerson: ObservableObject {
    @Published private var wrapped: any Personable
    
    init(_ wrapped: any Personable) {
        self.wrapped = wrapped
    }
    
    subscript<T>(dynamicMember keyPath: KeyPath<any Personable, T>) -> T {
        wrapped[keyPath: keyPath]
    }
    
    subscript<T>(dynamicMember keyPath: WritableKeyPath<any Personable, T>) -> T {
        get { wrapped[keyPath: keyPath] }
        set { wrapped[keyPath: keyPath] = newValue }
    }
}