@StateObject

@StateObject는 ObservableObject를 준수하는 클래스를 유지함.

SwiftUI를 사용하다 보면 뷰를 처음부터 다시 그려야 할 때가 있다. 이 말은 @StateObject를 사용하는 객체를 제외한 모든 객체가 재설정되는 것을 의미한다.
그 후 뷰가 다시 생성될 때, 해당 ObservableObject클래스는 유지된다.

순서

  1. 뷰를 다시 그리라고 신호를 보냄
  2. @StateObject 객체는 뷰 외부에서 유지됨
  3. @StateObject 값은 유지된 상태로 뷰가 다시 그려짐


import SwiftUI

class Idol: ObservableObject {
    @Published var group: String
    @Published var member: String
    
    init(group: String, member: String) {
        self.group = group
        self.member = member
    }
}

struct AboutStateObject: View {
    @StateObject private var idol = Idol(group: "aespa", member: "Karina")
    
    var body: some View {
        VStack(spacing: 15) {
            CommonTextView(title: "@StateObject", desc: "클래스와 UI 간의 양방향 바인딩에 @StateObject 속성 래퍼 사용")
            
            Text("\(idol.group) \(idol.member)")
                .font(.largeTitle)
                .fontWeight(.black)
            
            VStack {
                TextField("그룹", text: $idol.group)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("멤버", text: $idol.member)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }
            .padding()
        }
    }
}

위 코드를 실행하고 TextField의 텍스트를 수정하면, Text뷰의 텍스트 역시 실시간으로 변경되는 것을 볼 수 있음.
즉, 뷰를 다시 그리거나 새로 고친 경우에도 @StateObject인 idol변수는 현재 값을 잃지 않고 보존하는 것을 알 수 있다.


@State와의 비교

import SwiftUI

class Idol: ObservableObject {
    @Published var group: String
    @Published var member: String
    
    init(group: String, member: String) {
        self.group = group
        self.member = member
    }
}

struct AboutStateObject: View {
    @State private var idol = Idol(group: "aespa", member: "Karina")
    var body: some View {
        VStack(spacing: 15) {
            CommonTextView(title: "@StateObject와 @State비교", desc: "@State를 class와 함께 사용시, 양방향 바인딩 불가능. @StateObject는 오직 클래스와 바인딩되며, 구조체는 해당되지 않는데.")
            
            Text("\(idol.group) \(idol.member)")
                .font(.largeTitle)
                .fontWeight(.black)
            
            VStack {
                TextField("그룹", text: $idol.group)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("멤버", text: $idol.member)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }
            .padding()
        }
    }
}

이제 idol을 @StateObject에서 @State로 변경하고 다시 실행해보자.
TextField의 텍스트를 수정해도 Text뷰에 아무런 변화가 없음을 볼 수 있다.


Picker와 함께 사용하기

class MyMember: ObservableObject {
    @Published var member: [String] = [""]
    @Published var myPick = ""
    
    init() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [self] in
            member = ["Karina", "Winter", "NingNing", "Giselle"]
            myPick = member[1]
        }
    }
}

struct MyMemberWithPicker: View {
    @StateObject private var myMember = MyMember()
    
    var body: some View {
        VStack(spacing: 15) {
            CommonTextView(title: "@StateObject", desc: "@StateObject와 피커 바인딩하기")
            
            Text("멤버 선택")
            Picker(selection: $myMember.myPick) {   // 피커와 myMember 양방향 바인딩
                ForEach(myMember.member, id: \.self) { member in
                    Text(member).tag(member)
                }
            } label: {
                Text("Picker")
            }
            .pickerStyle(.wheel)
            .background(Color.orange.opacity(0.4))
            
            Text("현재 선택한 멤버 : ")
            + Text(myMember.myPick)
                .bold()
                .foregroundColor(.orange)

        }
    }
}

위 코드를 실행하면, 2초 후 member속성이 업데이트되고 UI로 푸쉬된다. 그리고 UI가 업데이트되는 것을 볼 수 있다.


@StateObject를 사용한 바인딩

class WithBinding: ObservableObject {
    @Published var color = Color.blue
    @Published var date = Date()
    @Published var slider = 0.75
    @Published var stepper = 50
    @Published var text = "단방향 바인딩"
    @Published var textField = "양방향 바인딩"
    @Published var textEditor = "TextEditor 데이터"
    @Published var toggle = true
}

struct StateObjectWithBinding: View {
    @StateObject private var examples = WithBinding()
    
    var body: some View {
        VStack(spacing: 20) {
            CommonTextView(title: "@StateObject", desc: "@StateObject를 사용해 다른 뷰들과 바인딩하기")
            
            Form {
                ColorPicker("Color Picker", selection: $examples.color)
                DatePicker(selection: $examples.date) {
                    Text("Date")
                }
                Slider(value: $examples.slider)
                Stepper(value: $examples.stepper, in: 0...100) {
                    Text("Value: \(examples.stepper)")
                }
                Text(examples.text)
                TextField("Placeholder", text: $examples.textField)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextEditor(text: $examples.textEditor)
                    .border(Color.green)
                Toggle("Toggle", isOn: $examples.toggle)
            }
        }
    }
}

이렇게 상호작용시 ObservableObject객체가 업데이트되고, ObservableObject객체가 업데이트되면 UI도 함께 업데이트된다.


@StateObject와 ObservableObject는 규칙이다

@StateObject는 ObservableObject를 준수하는 클래스와 함께 사용해야 한다.
그렇지 않으면 @StateObject를 이용할 수 없다 (Xcode에서 오류 발생함).



프리뷰에서 @StateObject 사용하기

import SwiftUI

class Idol: ObservableObject {
    @Published var group: String
    @Published var member: String
    
    init(group: String, member: String) {
        self.group = group
        self.member = member
    }
}

struct AboutStateObject: View {
    @StateObject var idol: Idol = Idol(group: "", member: "")
    
    var body: some View {
        VStack(spacing: 15) {
            CommonTextView(title: "@StateObject", desc: "프리뷰에서 @StateObject 사용하기")
            
            Text("\(idol.group) \(idol.member)")
                .font(.largeTitle)
                .fontWeight(.black)
            
            VStack {
                TextField("그룹", text: $idol.group)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("멤버", text: $idol.member)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }
            .padding()
        }
    }
}


/// 프리뷰
struct AboutStateObject_Previews: PreviewProvider {
    static var karina = Idol(group: "aespa", member: "카리나")
    static var winter = Idol(group: "aespa", member: "윈터")
    
    static var previews: some View {
        Group {
            AboutStateObject(idol: karina)
                .previewLayout(.sizeThatFits)
            AboutStateObject(idol: winter)
                .previewLayout(.sizeThatFits)
                .preferredColorScheme(.dark)
        }
    }
}
  • @StateObject가 private이면 안됨 (너무 당연함...).
  • 위 코드처럼 프리뷰에서 static 변수 선언해주기 (왜 static? : 프리뷰의 속성이 static 이니까)
  • 각 뷰의 이니셜라이저에 해당 변수 전달하기

@StateObject 정리

  • 뷰에서 class 속성을 업데이트하려고 할 때 사용
  • 앱의 한 화면에 대해서만 인스턴스화하고 유지하려는 클래스가 있을 때 사용
  • ObservableObject 클래스의 데이터와 양방향 바인딩을 원할 때 사용

즉, 양방향 바인딩을 제공하는 앱의 화면에 대해서만 인스턴스화하고 유지해야 하는 ObservableObject 클래스(레퍼런스 타입)가 있을 때 @StateObject를 사용함.

Struct(밸류 타입)의 유지를 원한다면 @State를 사용할 것

profile
안다고 착각하지 말기

0개의 댓글