SwiftUI - ObservableObject, @Published, @ObservedObject, @State, @EnvironmentObject

YSYD·2021년 3월 22일
0

SwiftUI

목록 보기
2/3

SwiftUI로 바인딩을 하려면 아래 다섯가지를 알아둬야한다.
1. ObservableObject protocol

protocol ObservableObject : AnyObject
  • ObservableObject 프로토콜을 따르는 클래스의 인스턴스를 관찰하고 있다가 값이 변경되면 뷰를 업데이트 한다.
class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func haveBirthday() -> Int {
        age += 1
        return age
    }
}

let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
    .sink { _ in
        print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

2. @Published

@propertyWrapper struct Published<Value>
  • ObservableObject에서 property를 선언할 때 사용하는 PropertyWrapper
  • @Published로 선언해두면 이 type에 대해 publisher를 만든다.
  • 접근은 이 publisher에 $ operator를 이용해서 접근한다.
  • 프로퍼티가 변경되면 프로퍼티의 willSet block 에 publishing이 발생하는데, 실제 값이 set되기 전에 새로운 값을 받는다는걸 의미한다.
class Weather {
    @Published var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}

let weather = Weather(temperature: 20)
cancellable = weather.$temperature
    .sink() {
        print ("Temperature now: \($0)")
}
weather.temperature = 25

// Prints:
// Temperature now: 20.0
// Temperature now: 25.0

3. @ObservedObject

@propertyWrapper @frozen struct ObservedObject<ObjectType> where ObjectType : ObservableObject
  • ObservableObject를 구독하고 값이 업데이트 될 때 마다 뷰를 갱신하는 PropertyWrapper
    (이거는 개발자사이트에 예시가 없어서 구글링으로 가져옴)
class UserProgress: ObservableObject {
    @Published var score = 0
}

struct InnerView: View {
    @ObservedObject var progress: UserProgress

    var body: some View {
        Button("Increase Score") {
            progress.score += 1
        }
    }
}

struct ContentView: View {
    @StateObject var progress = UserProgress()

    var body: some View {
        VStack {
            Text("Your score is \(progress.score)")
            InnerView(progress: progress)
        }
    }
}

4. @State

@frozen @propertyWrapper struct State<Value>
  • SwiftUI에서 값을 읽고 쓸 수 있는 property wrapper
  • state property를 다른 view에 넘기고싶을 땐 $ prefix operator를 이름앞에 넣으면 된다.
  • state로 선언한 값이 변경되면 view는 지금 보여지는 것들을 무효화하고 body를 다시 계산한다.
  • state 인스턴스는 값이 아닌, 읽고쓸수 있는 수단으로 이해해야한다.
  • view의 body안에서랑 state property에 의해 호출된 메소드에서만 접근가능하므로 private으로 선언해야한다. 그래야 어떤 스레드에서 이 state property를 수정하건 안전할 것이다.
    아래는 예제코드
struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false

    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(isPlaying: $isPlaying)
        }
    }
}

5. @EnvironmentObject

@frozen @propertyWrapper struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject
  • 상위 뷰에서 제공하는 Observable object에 대한 property wrapper type으로 이 객체가 변경되면 현재의 view를 무효시킨다.
  • 프로퍼티를 environment object로 선언하면 environmentObject(_:) 를 호출해서 상위뷰가 들고있는 모델 오브젝트에 set될 수 있도록 해야함.
    (아래는 구글링으로 가져온 예제코드)
  • 코드를 보면 상위뷰(ContentView)에서 @EnvironmentObject 로 정의한 settings 프로퍼티를 하위뷰에서 @EnvironmentObject로 선언해서 상위뷰에서 변경된 내용을 하위뷰에서도 반영할수있도록해두었다(개꿀인듯)
  • 위의 설명대로 SceneDelegate.swift에서 environmentObject(_:)를 호출해서 상위 뷰에 모델객체를 set해준다.
class GameSettings: ObservableObject {
    @Published var score: Int = 0
}
//SceneDelegate.swift file
var settings = GameSettings()
//SceneDelegate의 scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 

if let windowScene = scene as? UIWindowScene {
  let window = UIWindow(windowScene: windowScene)
  window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings))
  self.window = window
  window.makeKeyAndVisible()
}
//SceneDelegate.swift file end 
struct ContentView: View {
    // 1.
    @EnvironmentObject var settings: GameSettings
    
    var body: some View {
        NavigationView {
            VStack {
                // 2.       
                Stepper(value: $settings.score, in: 1...1000000, step: 1000,  label: {
                    Text("Current Score:  \(settings.score)")
                }).padding()
                // 3.
                NavigationLink(destination: ScoreView()) {
                    Text("Show Current Score")
                }
            }
        }
    }
}

struct ScoreView: View {
    @EnvironmentObject var settings: GameSettings
    
    var body: some View {
        Text("Score: \(settings.score)")
    }
}

아래는 추가로 찾은 예제코드(@StateObject라는걸 사용하고있다. iOS14지원)

// Our observable object class
class GameSettings: ObservableObject {
    @Published var score = 0
}

// A view that expects to find a GameSettings object
// in the environment, and shows its score.
struct ScoreView: View {
    @EnvironmentObject var settings: GameSettings

    var body: some View {
        Text("Score: \(settings.score)")
    }
}

// A view that creates the GameSettings object,
// and places it into the environment for the
// navigation view.
struct ContentView: View {
    @StateObject var settings = GameSettings()

    var body: some View {
        NavigationView {
            VStack {
                // A button that writes to the environment settings
                Button("Increase Score") {
                    settings.score += 1
                }

                NavigationLink(destination: ScoreView()) {
                    Text("Show Detail View")
                }
            }
            .frame(height: 200)
        }
        .environmentObject(settings)
    }
}
profile
YSYD

0개의 댓글