SwiftUI

SwiftUI - 상태 프로퍼티 (State), Observable 객체, Environment 객체 사용

iosos 2024. 1. 15. 14:59
  • SwiftUI는 데이터 주도 방식으로 앱 개발을 강조
  • 사용자 인터페이스 내의 뷰들은 기본 데이터의 변경에 따른 처리 코드를 작성하지 않아도 뷰가 업데이트 됨
    • 데이터와 사용자 인터페이스 내의 뷰 사이에 게시자(publisher)와 구독자(subscriber)를 구축하여 가능
    • 이를 위해 SwiftUI는 상태 프로퍼티, Observable 객체, Environment 객체를 제공
    • 이들은 사용자 인터페이스의 모양과 동작을 결정하는 상태를 제공함
  • SwiftUI에서 레이아웃을 구성하는 뷰는 코드 내에서 직접 업데이트 하지 않음 → 뷰와 바인딩된 상태 객체가 시간이 지남에 따라 변하면 그 상태에 따라 자동으로 뷰 업데이트 됨

 

 

 

상태 프로퍼티 (State Property)

  • 상태에 대한 가장 기본적인 형태
  • 뷰 레이아웃의 현재 상태(토글 버튼 활성화 여부, 텍스트 필드에 입력된 텍스트, 피커뷰의 현재 선택)을 저장하기 위해 사용됨
  • String, Int 값 처럼 간단한 데이터 타입을 저장하기 위해 사용됨
  • @State 프로퍼티 래퍼를 사용하여 선언
struct Chap21: View {
    
    @State private var wifiEnabled = true
    @State private var userName = ""
    
    var body: some View {
        Text("Hello")
    }
}

→ 해당 값은 뷰에 속한 것이기 때문에 private 프로퍼티로 선언됨

 

 

 

  • 상태 프로퍼티 값이 변경된 것은 프로퍼티가 선언된 뷰 계층구조를 다시 렌더링해야 한다는 신호!
    • 즉, 계층 구조 내 모든 뷰를 빠르게 재생성하고 표시해야 함
    • 그 프로퍼티에 의존하는 모든 뷰는 어떤 식으로든 최신 값이 반영되도록 업데이트 됨
  • 프로퍼티 선언 후 레이아웃에 있는 뷰와 바인딩 가능함
  • 바인딩 되어 있는 뷰에서 어떤 변경이 일어나면 해당 상태 프로퍼티에 자동으로 반영됨
  • ex) wifiEnabled 프로퍼티가 바인딩되어 사용자가 토글 뷰를 조작하면 새로운 토글 설정 값으로 상태 프로퍼티를 자동 업데이트 해야 됨
    • 상태 프로퍼티와의 바인딩은 프로퍼티 이름 앞에 ‘$’ 를 붙이면 됨
  • ex) TextField 뷰는 사용자가 입력한 텍스트를 저장하는 데 사용하기 위해 userName이라는 상태 프로퍼티와 바인딩 됨
struct Chap21: View {
    
    @State private var wifiEnabled = true
    @State private var userName = ""
    
    var body: some View {
        VStack{
            TextField("Enter user name", text: $userName)
        }
    }
}

→ TextField에 입력하게 되면 바인딩은 현재의 텍스트를 userName이라는 프로퍼티에 저장함

 

 

  • 이 상태 프로퍼티에 변화가 생길 때마다 뷰 계층구조는 SwiftUI에 의해 다시 렌더링 됨
  • 상태 프로퍼티에 값을 저장하는 것은 단방향 프로세스
  • ex) 입력된 사용자의 이름을 반영하는 Text 뷰 생성
struct Chap21: View {
    
    @State private var wifiEnabled = true
    @State private var userName = ""
    
    var body: some View {
        VStack{
            TextField("Enter user name", text: $userName)
            Text(userName)
        }
    }
}

 

→ 텍스트를 입력하면 Text 뷰는 입력을 반영하기 위해 자동으로 업데이트 됨

 

  • 중요! : userName 프로퍼티는 ‘$’ 표시 없이 사용되어야 함
    • 상태 프로퍼티에 할당된 값(즉, 입력된 String 값)을 참조하려고 하기 때문

 

 

ex) Toggle 뷰와 wifiEnabled 상태 프로퍼티 간의 바인딩

struct Chap21: View {
    
    @State private var wifiEnabled = true
    @State private var userName = ""
    
    var body: some View {
        VStack{
            Toggle(isOn: $wifiEnabled){
                Text("Enable Wi-Fi")
            }
        }
        TextField("Enter user name", text: $userName)
        Text(userName)
        Image(systemName:  wifiEnabled ? "wifi" : "wifi.slash")
    }
}

 

→ Toggle 뷰와 상태 프로퍼티 간의 바인딩 구현

  • Image 뷰 : systemName 이미지 참조체를 이용함

 

 

 

 

 

상태 바인딩

  • 상태 프로퍼티 : 선언된 뷰와 그 하위 뷰에 대한 현재 값
  • 어떤 뷰가 하나 이상의 하위 뷰를 가지고 있고, 동일한 상태 프로퍼티에 접근하는 경우가 발생
  • ex) 와이파이 뷰가 하위 뷰로 분리되는 상황
struct Chap21: View {
    
    @State private var wifiEnabled = true
    @State private var userName = ""
    
    var body: some View {
        VStack{
            Toggle(isOn: $wifiEnabled){
                Text("Enable Wi-Fi")
            }
        }
        TextField("Enter user name", text: $userName)
        Text(userName)
        WifiImageView()
        
    }
}

struct WifiImageView : View {
    var body: some View{
	// 오류 : Cannot find 'wifiEnabled' in scope
        Image(systemName:  wifiEnabled ? "wifi" : "wifi.slash")
    }
}

→ 분리된 하위 뷰의 요소인 Image 뷰는 메인 뷰 범위 밖이므로 wifiEnabled는 정의되지 않은 변수가 됨

 

 

  • @Binding 프로퍼티 래퍼를 사용하여 해결 가능
struct Chap21: View {
    
    @State private var wifiEnabled = true
    @State private var userName = ""
    
    var body: some View {
        VStack{
            Toggle(isOn: $wifiEnabled){
                Text("Enable Wi-Fi")
            }
        }
        TextField("Enter user name", text: $userName)
        Text(userName)
        WifiImageView(wifiEnabled: $wifiEnabled)
    }
}

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

→ 하위 뷰가 호출될 때 상태 프로퍼티에 대한 바인딩을 전달해야 함

 

 

 

 

 

Observable 객체

  • 상태 프로퍼티는 하위 뷰가 아니거나 상태 바인딩이 구현되어 있지 않은 다른 뷰는 접근할 수 없음
  • 상태 프로퍼티는 일시적인 것이라 부모 뷰가 사라지면 그 상태도 사라짐
  • Observable 객체는 여러 다른 뷰들이 외부에서 접근 할 수 있는 영구적인 데이터를 표현하기 위해 사용됨
  • Observable 객체는 ObservableObject 프로토콜을 따르는 클래스나 구조체 형태를 취함
  • Observable 객체는 일반적으로 시간에 따라 변경되는 하나 이상의 데이터 값을 모으고 관리하는 역할
  • 또한, 타이머나 알림(notification)과 같은 이벤트 처리를 위해 사용됨
  • Observable 객체는 게시된 프로퍼티 (published property)로서 데이터 값을 게시(publish) 한 후 Observer 객체는 게시자를 구독(subscribe)하여 게시된 프로퍼티가 변경될 때마다 업데이트 받음
  • 상태프로퍼티와 마찬가지로 게시된 프로퍼티와 바인딩을 통해 데이터가 변경됨을 반영함
  • Combine 프레임워크
    • Combine 프레임워크에 포함되어 있는 Observable 객체는 게시자와 구독자 간의 관계를 쉽게 구축 가능
    • 여러 게시자를 하나의 스트림으로 병합하는 것부터 게시된 데이터를 구독자 요구에 맞게 변형하는 것까지 다양한 작업을 수행하는 커스텀 게시자 구축 플랫폼을 제공함
    • 최초 게시자와 최종 구독자 간에 복잡한 수준의 연쇄 데이터 처리 작업도 구현 가능
    • Observable 객체의 게시된 프로퍼티를 구현하는 가장 쉬운 방법
      • 프로퍼티 선언 시 @published 프로퍼티 래퍼 사용하는 것
      • 이 래퍼는 프로퍼티 값이 변경될 때마다 모든 구독자에게 업데이트를 알림

 

 

ex) 두 개의 게시된 프로퍼티를 가진 간단한 observable 객체(DemoData) 선언

import Foundation
import Combine

// Observable 객체
class DemoData : ObservableObject {
	// 게시
    @Published var userCount = 0
    @Published var currentUser = ""
    
    init(){
        // 데이터 초기화 코드
        updateData()
    }
    func updateData(){
        // 데이터를 최신 상태로 유지하기 위한 코드
    }
}

 

 

 

  • 구독자는 observable 객체를 구독하기 위해 @ObservedObejct 프로퍼티 래퍼를 사용함
  • 구독하게 되면 상태프로퍼티와 같은 방법으로 게시된 프로퍼티에 접근함

ex) DemoData 클래스의 인스턴스를 구독하도록 설계된 예제

import SwiftUI

struct Chap21: View {
    
    @ObservedObject var demoData : DemoData
    
    var body: some View {
        Text("\(demoData.currentUser), you are user number \(demoData.userCount)")
            
    }
}

#Preview {
    Chap21(demoData: DemoData())
}

 

 

 

 

 

Environment 객체

  • 구독 객체는 특정 상태가 앱 내의 몇 몇 뷰에 의해 사용될 경우 적합함
  • 다른 뷰로 이동(navigation)할 때 이동될 뷰에서도 동일한 구독 객체를 접근해야 한다면, 이동할 대상 뷰로 구독 객체에 대한 참조체를 전달해야 함
...

@ObservedObject var demoData : DemoData = DemoData()
    
...

    NavigationLink(destination: SecondView(DemoData)){
        Text("Next Screen")
    }

→ SecondeView 라는 이름의 다른 뷰로 이동하면서 demoData 객체에 대한 참조체 전달

 

 

 

 

  • 앱 내에 여러 뷰가 동일한 구독 객체를 접근하는 경우 복잡함 → Environment 객체 사용하여 해결
  • Environment 객체는 Observable 객체와 같은 방식으로 선언
    • 즉, ObservableObject 프로토콜을 따르며, 적절한 프로퍼티가 게시되어야 함
  • 차이점 : Environment 객체는 SwiftUI 환경에 저장되며, 모든 뷰에서 접근 가능
  • Environment 객체를 구독해야 하는 객체는 @ObservedObject 래퍼 대신 @EnvironmentObject 프로퍼티 래퍼 를 사용하여 참조
@EnvironmentObejct var demoData : DemoData
  • Environment 객체는 옵저버 내에서 초기화 불가능하므로 접근하는 뷰가 화면 설정하는 동안 구성해야 함

→ SceneDelegate.swift 파일의 willConnectTo 메서드를 수정하는 작업이 필요함

let contentView = ContentView()

if let windowScene = scne as? UIWindowScene{
	let window = UIWindow(windowScene : windowScene)
	window.rootViewController = UIHostingController(rootView: contentView)
	self.window = window
	window.makeKeyAndVisible()
}

// -> default code 
// 아래와 같이 수정

let contentView = ContentView()

let demoData = DemoData()

if let windowScene = scne as? UIWindowScene{
	let window = UIWindow(windowScene : windowScene)
	window.rootViewController = UIHostingController(rootView: contentView)
	self.window = window
	window.makeKeyAndVisible()
}

 

 

 

 

 

  • 캔버스 수정
#Preview {
    Chap21().environment(DemoData())
}