본문 바로가기
2023년 이전/iOS

SwiftUI - 상태, Observable 객체, Environment 객체

by JeongUPark 2020. 10. 4.
반응형

다음 내용들은 [핵심만 골라배우는 SwiftUI 기반 iOS 프로그래밍] 을 공부하면서 정리한 내용 입니다.

 

SwiftUI는 데이터 주도 방시긍로 앱 개발을 강조합니다. 사용자 인터페이스 내의 뷰들은 기본 데이터의 변경에 따른 처리 코드를 작성하지 않아도 뷰가 업데이트 됩니다. 이것은 데이터와 사용자 인터페이스 내의 뷰 사이에 게시자와 구독자를 구축하여 할 수 있습니다

 

위의 설명과 관련하여 이번에는 상태 프로퍼티, Observable 객체, Environment 객체에 대해 설명해 보겠습니다.

 

1.상태 프로퍼티

상태 프로퍼티는(State Propertiy) 상태에 대한 가장 기본적이 형태이며, 뷰 레이아웃의 현재 상태 (토클 버튼 활성화 여부, 텍스트 필드에 입력된 텍스트 또는 피커 뷰에서 현재 선택)을 저장하기 위해서만 사용됩니다. 

 

그럼 예를 들어 사용 법을 확인해 보겠습니다.

import SwiftUI

struct ContentView: View {
    @State private var wifiEnable : Bool = true
    @State private var userName : String = ""

    var body: some View {
        VStack{
            Toggle(isOn: $wifiEnable, label: {
                Text("Wifi Enable")
            })
            Text("wifiEnable: \(wifiEnable.description)")
            TextField("Enter User Name", text: $userName)
            Text(userName)
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

위와 같이 사용자 동작에 따라 그 상태 값들이 변경되고 최신값을 계속 확인 할 수 있습니다.

여기서 private를 설정한 이유는 이 state 값들은 해당 뷰에만 속하기 때문에 private를 설정합니다. 

 

 

2.상태 바인딩

상태값을 하위 뷰에 적용하는 방법은 다음과 같습니다.

import SwiftUI

struct ContentView: View {
    @State private var wifiEnable : Bool = true
    @State private var userName : String = ""

    var body: some View {
        VStack{
            wifiSubView(wifistate: $wifiEnable)
            TextField("Enter User Name", text: $userName)
            Text(userName)
        }
    }
}
struct wifiSubView : View{
    @Binding var wifistate : Bool
    var body: some View{
        Toggle(isOn: $wifistate, label: {
            Text("Wifi Enable")
        })
        Text("wifiEnable: \(wifistate.description)")
    }
}

이렇게 Binding 속성을 통하여 @State로 설정한 상태 프로퍼티를 바인딩 하여 하위 뷰에 적용할 수 있습니다.

 

 

3.Observable 객체

상태 프로퍼티는 해당 뷰에서만 사용하거나 Binding을 통하여 하위 뷰에서 사용할 수 있습니다. 하지만 Observable 객체는 여러 다른 뷰들이 외부에서 접근 할 수 있는 영구적이 데이터를 표현하기 위해 사용됩니다.

 

Observable 객체는 일반적으로 시간에 따라 변경되는 하나 이상의 데이터 값을 모으고 관리하는 역할을 담당합니다.( 예를 들어 타이머나 알림과 같은 이벤트 처리를 위해 사용)

 

Observable 객체는 publushed를 통해 게시하고 , subscribe를 통하여 게시된 프로퍼티가 변경될 때마다 업데이트를 받습니다.

이게 가능한 이유는 Combine 프레임워크가 iOS13 부터 도입되었기 쉽게 구축할 수 있게 되었기 때문입니다. (Combine에 대해서는 따로 공부해야할 듯합니다.)

 

사용 방법을 보겠습니다.

TestData.swfit 파일을 만듭니다. 그리고 ObservableObject를 상속한 class 를 만듭니다.

import Foundation
import Combine

class TestData : ObservableObject {
    
    @Published var count = 0

    
    func update(){
        count += 1
    }
}

그리고 ContentView를 다음과 같이 변경합니다.

import SwiftUI
import Combine

struct ContentView: View {
    
    @ObservedObject var testData : TestData

    var body: some View {
        VStack{
            Text("current testDATA : \(testData.count)")
            Button(action: {
                testData.update()
            }, label: {
                Text("Add Count")
            })
        }
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(testData: TestData())
    }
}

 

이렇게 변경할 경우 Error가 나타나는데 그 원인은 

SceneDelegate.swift 에 있는 ContentView도 위의 ContentView_PreViews 에 있는  ContentView(testData: TestData()) 처럼 바꿔줘야하기 때문입니다.

 

이렇게 작성하면 

이런 결과 화면ㅇ르 볼 수 있고 Add Count 하면 숫자가 계속 1씩 증가하는 것을 확인 할 수 있습니다.

 

즉 publish 한 count에 대하여 ObserveredObject 가 계속 지켜보면서 변화를 캐치하고 그 변화를 적용한다고 생각하면 됩니다.

 

4. Environment 객체

Observable 객체는 특정 상태가 앱 내의 몇몇 뷰에의 해 사용될 때는 적합하지만, 뷰에서 다른뷰로 이동될 때 동일한 구독 객체에 접근해야하는 문제 점이 있습니다.

import SwiftUI
import Combine

struct ContentView: View {
    
    @ObservedObject var testData : TestData
    
    var body: some View {
        NavigationView{
            VStack{
                Text("current testDATA : \(testData.count)")
                Button(action: {
                    testData.update()
                }, label: {
                    Text("Add Count")
                })
                
                NavigationLink(
                    destination: NextView(testData: testData),
                    label: {
                        Text("Next")
                    })
            }
        }
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(testData: TestData())
    }
}

위와 같이 NextView에 그 Observable 객체를 전달해야 합니다.

 

반면 Environment 객체는 SwiftUI 환경에 저장되며, 뷰에서 뷰로 전달할 필요 없고, 모든 뷰에서 접근이 가능합니다.

 

사용 방법은 다음과 같습니다.

먼저 SceneDelegate.swift 를 다음과 수정 합니다.

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

//위의 func을 아래와 같이 수정 합니다.
 
 
 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()
        let testData = TestData()
        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(testData))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

그리고 ContentView 를 다음과 같이 수정 합니다.

import SwiftUI
import Combine

struct ContentView: View {
    
    @EnvironmentObject var testData : TestData
    
    var body: some View {
        NavigationView{
            VStack{
                Text("current testDATA : \(testData.count)")
                Button(action: {
                    testData.update()
                }, label: {
                    Text("Add Count")
                })
                
                NavigationLink(
                    destination: NextView(),
                    label: {
                        Text("Next")
                    })
            }
        }
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(TestData())
    }
}

이렇게 하면 ObservableObject 객체를 전달할 필요 없이 모든 뷰에서 접근하여 사용할 수 있습니다.

 

NextView는 다음과 같고

import SwiftUI

struct NextView: View {
    @EnvironmentObject var testData : TestData
    var body: some View {
        Text("Hello, NextView! count: \(testData.count)")
    }
}

struct NextView_Previews: PreviewProvider {
    static var previews: some View {
        NextView().environmentObject(TestData())
    }
}

ContentView에서 count를 올리고 NextView로 뷰를 바꾸면 변경된 Count가 똑같이 적용 되는 것을 확인 할 수 있습니다.

반응형