[Swift UI] 상태 프로퍼티, Observable, Environment 객체

리앤·2022년 10월 24일
1

SwiftUI는 데이터 주도 방식의 앱 개발을 강조한다?

  • SwiftUI는 사용자 인터페이스 내의 뷰들은 데이터의 변경에 따른 처리 코드를 작성하지 않아도 뷰가 자동적으로 업데이트 되게끔 해준다
  • 데이터와 사용자 인터페이스 내의 뷰 사이에 게시자(publisher)와 구독자(subscriber)를 구축해서 데이터를 주고받는 형식을 취한다

상태 프로퍼티, Observable 객체와 Environment 객체,
모두 사용자 인터페이스의 모양과 동작을 결정하는 "상태"를 제공

  • 뷰와 바인딩된 상태 객체의 값이 변경되면 그 상태에 따라 뷰가 업데이트 된다

 


상태 프로퍼티 @State

  • 상태에 대한 가장 기본적인 형태
  • 문자열이나 정수 값처럼 간단한 데이터 타입을 저장하기 위해 사용되며, @State 프로퍼티 래퍼를 사용하여 선언된다

 

상태 프로퍼티를 이용한 바인딩

  • input을 userName에 걸어주면 input 값이 바뀔 때마다 Text()에 들어가는 userName 값이 자동적으로 바뀌게 된다 (바인딩)
    $variableName → 바인딩을 걸어줄 때 $를 붙여줌
    variableName → 참조된 변수를 호출할 때는 변수명만 사용함
struct ContentView: View {
    @State private var userName: String = ""
    
    var body: some View {
        VStack {
            TextField("이름을 입력하세요", text: $userName)
            Text(userName)
        }
    }
}

 
기존 뷰 이외의 곳에서 상태 변수 사용
@Binding를 이용한 직접적인 상태 바인딩

  • WifiResultView 구조체를 메인뷰 (ContentView) 범위 밖에서 생성하려면, private으로 정해진 userName 값은 구조체 구현부에서 쓸 수가 없음
  • 그래서 새로 wifiEnable 변수를 선언해줘서 써야 하는데, 문제는 이 구조체의 wifiEnable와 메인뷰 안에 있는 wifiEnable 변수가 같은 값이 아님
    @Binding을 프로퍼티 래퍼를 이용해 프로퍼티를 연결시켜주면 됨!
struct ContentView: View {
    @State private var userName: String = ""
    @State private var wifiEnable: Bool = false
    var body: some View {
        VStack {
            Toggle(isOn: $wifiEnable) {
                WifiResultView(wifiEnable: $wifiEnable)
                }
            }
            TextField("이름을 입력하세요", text: $userName)
                . padding(5)
                .border(Color.gray)
            Text(userName)
                .foregroundColor(.gray)
                .font(.callout)
        }
        .padding()
        .font(.largeTitle)
    }
}

struct WifiResultView: View {
    @Binding var wifiEnable: Bool
    
    var body: some View {
        Image(systemName: wifiEnable ? "wifi" : "wifi.slash")
        Text(wifiEnable ? "Wi-Fi 켜짐" : "Wi-Fi 꺼짐")
    }
}

 

  • 구조체 호출 시에 매개변수로 (변수명: $변수명) 넣어서 바인딩 해주기
    "wifiEnable라는 변수를 쓸 건데: $wifiEnable 값이랑 연결해주세요"
  • @State private는 해당 뷰 안에서만 사용되고, 프로퍼티의 값이 바뀌면 화면이 다시 그려진다
  • 일반 변수를 매개변수로 사용하는 것 → 읽기전용 느낌 (그냥 갖다 쓰는 격)
  • $가 붙은 바인딩된 매개변수를 사용하는 것 → 바인딩 프로토콜로 인해 값이 연동됨 (갖다 쓰고 변경 값도 전해준다)
struct ContentView: View {
    @State private var wifiEnable: Bool = true
    @State private var username: String = ""
    
    var body: some View {
        VStack {
            Toggle(isOn: $wifiEnable) {
                Text("Enable Wifi")
            }
            TextField("Enter Username", text: $username)
            Text("username: \(username)")
            
            WifiImageView(wifiEnable: $wifiEnable)
        }
        .padding()
    }
}

struct WifiImageView: View {
    @Binding var wifiEnable: Bool
    
    var body: some View {
        VStack {
            Image(systemName: wifiEnable ? "wifi" : "wifi.slash")
...

 

하지만... 상태 뷰는 일시적이라 부모 뷰가 사라지면 해당 상태도 사라진다는 것..🥲
그래서 나왔다, Observable.

 

Observable 객체

  • Observable 객체는 ObservableObject 프로토콜을 따르는 클래스나 구조체 형태를 취한다
  • 일반적으로 시간에 따라 변경되는 하나 이상의 데이터 값을 모으고 관리하는 역할 담당
    “바뀔 때마다 내가 알아서 바꿔놓을게~” (State의 확장판 정도?)
  • Observable 객체는 클래스로만 만들 수 있음 (구조체 안됨)

 

게시자와 구독자의 개념

  • Observable 객체는 게시된 프로퍼티(published property)로, 데이터 값을 게시한다
  • 그 다음 Observer 객체는 게시자를 구독(subscribe)해서 게시된 프로퍼티가 변경될 때마다 업데이트를 받는 식으로 동작한다
  • 상태 프로퍼티처럼, 게시된 프로퍼티와의 바인딩을 통해 Observable 객체에 저장된 데이터가 변경됨을 반영하기 위해 SwiftUI 뷰는 자동으로 업데이트 된다

Observable 객체 - 프로퍼티를 선언할 때 @Published 프로퍼티 래퍼를 사용해 게시된 프로퍼티 구현하면 래퍼 프로퍼티 값이 변경될 때마다 모든 구독자에게 업데이트를 알려준다

Observer 객체 - 구독자는 observable 객체를 구독하기 위해 @ObservedObject 프로퍼티 래퍼를 사용한다 / 구독하게 되면 그 뷰 및 모든 하위 뷰가 상태 프로퍼티에서 사용했던 것과 같은 방식으로 게시된 프로퍼티에 접근하게 된다

 

Observable 객체 구현

  • Observable 객체를 사용하지 않으면, 아래 코드처럼 DemoData 안에서 userCount 값이 바껴도 ContentView에선 초기에 불러온 값만 적용이 됨
  • 값이 업데이트가 안된다 (새로 렌더링 안됨)
  • 이 경우 Observable 객체를 사용하게 되면 값이 업데이트 될 때마다 뷰가 새로 랜더링 됨
    (렌더링 범위는 그때그때 다르고, Swift가 알아서 해주기 때문에 개발자가 직접 제어할 필요 없다)
// 1. 내부 내용들이 바뀔 예정!
class DemoData: ObservableObject {
    
    // Published의 의미는 "다음과 값이 바뀌면 알려주겠다"는 뜻
    // 2. 구체적으로 이런 내용이 바뀔 예정!
    @Published var userCount: Int = 0
    @Published var currentUser: String = ""
    
    init() {
        updateData()
    }
    
    func updateData() {
        userCount += 1
        currentUser = "ned"
    }
}

struct ContentView: View {
    
    @ObservedObject var demoData: DemoData = DemoData()
    
    var body: some View {
        NavigationView {
            VStack {
                
                Text("userCount: \(demoData.userCount)")
                Text("currentUser: \(demoData.currentUser)")
                    .padding()
                
                Button(action: {
                    demoData.updateData()
                }) {
                    Text("Update Data")
                }
...

각 프로퍼티 래퍼의 역할:
@ObservableObject = “이거 좀 지켜봐줘!”
@Published = “얘네가 바뀔 예정이니까 얘네 값을 전달줄게”
@ObserverObject = “오케이 바뀐 값 좀 줘봐”

 

구독자 선언 시 초기화 생략

  • @ObservedObject var demoData: DemoData = DemoData() ← 이 초기화 부분을 생략해도 됨
  • 대신 프리뷰랑 메인 앱 파일에서 ContentView() 안에 매개변수로 전달해줘야 함
struct ContentView: View {
    
    @ObservedObject var demoData: DemoData
    
    var body: some View {
        NavigationView {
            VStack {
// 프리뷰 뷰
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(demoData: DemoData())
    }
}
// 앱 파일 
@main
struct StateWrapUpApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(demoData: DemoData())
        }
    }
}

 

Environment 객체

  • Environment 객체도 Observable 객체와 같은 방식으로 선언됨
  • 하지만 Environment 객체눈 Observable과 달리 SwiftUI 환경에 저장되며, 뷰에서 뷰로 전달할 필요 없이 모든 뷰가 접근 가능하다 (!)
struct ContentView: View {
    
    @EnvironmentObject var demoData: DemoData
    
    var body: some View {
        NavigationView {
            VStack {
                
                Text("userCount: \(demoData.userCount)")
                Text("currentUser: \(demoData.currentUser)")
                    .padding()
                
                Button(action: {
                    demoData.updateData()
                }) {
                    Text("Update Data")
                }
                
                NavigationLink(destination: SecondView()) {
                    Text("Push")
                        .padding()
                }
            }
 ...
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(DemoData())
    }
}
// 앱 파일 
@main
struct StateWrapUpApp: App {
    
    let demoData: DemoData = DemoData()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(demoData)
        }
    }
}

 

정리: State, bservable, Environment, 언제 뭘 쓰나?

  1. 한 화면에서 입력값을 받아 처리하는 상황 -> State
  2. 이어지는 화면에 걸쳐서 넘기고 영향을 받는 상황 -> Observable
  3. 거의 모든 화면에 걸치는 데이터를 사용하는 상황 -> Environment

 


Reference:
핵심만 골라 배우는 SwiftUI 기반의 iOS 프로그래밍

profile
iOS 뉴비의 성장 기록

0개의 댓글