DADAHAE's Log
비싼 장난감 가지고 노는 중 (❁´▽`❁)*✲゚*
[SwiftUI] Observable Object와 Environment Object에 대해 알아보자.
22-10-24 월요일
7주차

 

 

 

 

🦢 SwiftUI

스위프트 유아이의 State, Observable Object, Environment Object에 대해서 알아보자!

저번에 State에 대해서 먼저 공부했으니 간단히 리뷰를 하고 Observable Object, Environment Object에 대해서 알아보겠다.

 

 

 

 

📒 Review: State

state는 뷰 안에서 내용이 변경되려고 할 때 사용한다.

  • 선언된 뷰 안에서만 건드릴 수 있도록 private 를 붙여준다.
  • 별개의 구조체로 만든 하위 뷰에서 state 변수를 사용하려면 @Binding 변수로 선언해준다.

 

 

 

 

✔ State, Observable Object, Environment Object

Gosh Darn SwiftUI - Cheat Sheet

 

Gosh Darn SwiftUI - Cheat Sheet

Everything you need to know to adopt SwiftUI

goshdarnswiftui.com

위 자료에 좋은 내용이 많다. 참고하시길!

 

잠깐 바로 옵저버블에 대해서 이야기하기 전에, 데모에서 사용할 Navigation을 사용할 것이기 때문에 요 부분을 잠깐 짚고 넘어가겠다.

 

 

📌 Navigation

https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types

 

Apple Developer Documentation

 

developer.apple.com

이제 NavigationView 대신 NavigationStack을 사용해야 한다. 그러나 내가 참고한 책에서는 View로 쓰여 있기 때문에 일단 여기서는 NavigationView로 쓰겠다.

나중에 NavigationStack에 관해서는 따로 작성할 예정이다!

NavigationView {
    NavigationLink(destination:
        Text("Detail")
        .navigationBarTitle(Text("Detail"))
    ) {
        Text("Push")
    }.navigationBarTitle(Text("Master"))
}

// NavigaionStack
NavigaionStack {
    NavigationLink(destination:
        Text("Detail")
        .navigationBarTitle(Text("Detail"))
    ) {
        Text("Push")
    }.navigationBarTitle(Text("Master"))
}

완전히 똑같지는 않지만, NavigationView를 NavigationStack으로 바꾸어도 큰 문제가 없다. 자세한건 다른 글에서 설명할 예정이다.

 

 

1️⃣ Observable Object

여러 다른 뷰들이 외부에서 접근할 수 있는 영구적인 데이터를 표현하기 위해 사용된다.

상태 프로퍼티가 선언된 부모뷰와 그 하위뷰에서만 사용할 수 있는 것과 대조된다. 즉, 상태 바인딩이 구현되어 있지 않은 다른 뷰에서 접근할 수 있나 없냐의 차이이다.

Observable Object다른 뷰들이 외부에서 접근할 수 있는 영구적인 데이터를 표현하기 위해 사용된다.

  • 독립적인 존재의 데이터를 만들어두고, 스스로가 데이터를 업데이트 하거나 데이터를 받아와서 업데이트하거나…
  • 타이머, 알림과 같은 이벤트를 처리할 때 사용

 

  • Observable object
    • 게시, publish
  • Observed object
    • 구독, subscribe

 

Combine 프레임 워크에 포함되어 있는 Observable 객체publishersubscriber 간의 관계를 쉽게 구축할 수 있게 한다.

그러므로 state public이라는 것을 쓸 필요가 없다. 이를 뛰어넘은 Observable 이 있기 때문이다.

  • ObservableObject는 Class로만 만들 수 있다.

 

카운터를 직접 만들어보면서 Observable Object를 이해해보자.

import SwiftUI
import Combine

// ObservableObject의 의미는, '이거 지켜봐줘" 뜻
// 1. 내부의 내용들이 바뀔 예정!
class DemoData: ObservableObject  {
    
    // Published의 의미는 "다음의 값이 바뀌면 알려주겠다"는 뜻
    // 2. 구체적으로 이런 내용이 바뀔 예정!
    @Published var userCount: Int = 0
    @Published var currentUser: String = ""
    
    init() {
        updateData()
    }
    
    func updateData() {
        userCount += 1
        currentUser = "dadahae"
    }
}

// ContentView
struct ContentView: View {
    @ObservedObject var demoData: DemoData
    
    var body: some View {
        NavigationView {
            VStack {
								// 현재 카운터와 유저를 화면에 표시한다.
                Text("userCount: \\(demoData.userCount)")
                    .padding()
                Text("currentUser: \\(demoData.currentUser)")
                    .padding()
                
								// 카운터 버튼, 누를때마다 1씩 올라간다.
                Button(action: {
                    demoData.updateData()
                }) {
                    Text("Update data")
                }
                .padding()
                
								// SecondView로 이동
								// 이때 demoData에 대한 참조체를 SecondView로 전달한다.
                NavigationLink(destination: SecondView(demoData: demoData)) {
                    Text("Push")
                        .padding()
                }
            }
        }
    }
}

// SecondView
struct SecondView: View {
    @ObservedObject var demoData: DemoData
    
    var body: some View {
        NavigationView {
            VStack {
								// 현재 카운터와 유저를 화면에 표시한다.
                Text("userCount: \\(demoData.userCount)")
                    .padding()
                Text("currentUser: \\(demoData.currentUser)")
                    .padding()
                
								// 카운터 버튼, 누를때마다 1씩 올라간다.
                Button(action: {
                    demoData.updateData()
                }) {
                    Text("Update data")
                }
                .padding()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(demoData: DemoData())
    }
}
  • 첫번째 뷰(ContentView)에서 버튼을 눌러 카운트를 올릴 수 있다.
  • 두번째 뷰(SecondView)에서 버튼을 눌러 카운트를 올릴 수 있다.

📌 중요 포인트

동일한 ObservedObject 객체 구독 및 참조체 전달
  • 첫번째 뷰나, 두번째 뷰에서 동일한 ObservedObject(DemoData)를 사용하고 있다.
  • 그러므로 어떤 뷰에서 카운트를 올리든지 간에 동일하게 카운트됨을 확인할 수 있다.
  • 단, 두번째 뷰는 동일한 DemoData 객체에 대한 참조체(demoData)를 ContentView로 전달받았기 때문에 동일한 카운트를 사용할 수 있다.

 

 

2️⃣ Environment Object

ContentView, SecondView가 동일한 TimerData 객체에 대한 참조체를 전달하지 않아도 접근할 수 있다.

타이머를 직접 만들어보면서 Environment Object를 이해해보자.

 

  • TimerData.Swift
import Foundation
import Combine

// Environment Object를 쓰기 위해서는 동일하게 ObservableObject 클래스를 만들어준다.
class TimerData: ObservableObject {
    @Published var timeCount = 0
    var timer: Timer?
    
		// 이니셜라이저로 Timer에 대한 기본 인스턴스를 생성한다.
    init() {
        timer = Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(timerDidFire),
            userInfo: nil,
            repeats: true
        )
    }
    
		// timeInterval마다 timerDidFire 함수가 실행된다.
		// 현재는 1초에 1씩 증가하도록 설정하였다.
    @objc func timerDidFire() {
        timeCount += 1
    }
    
    func resetCount() {
        timeCount = 0
    }
    
}

 

  • ContentView.swift
import SwiftUI

struct ContentView: View {
		// ObservedObject와 다르게 EnvironmentObject로 래핑해준다.
    @EnvironmentObject var timerData: TimerData
    
    var body: some View {
        NavigationView {
            VStack {
								// 타이머를 화면에 보여준다. 타이머 객체는 뷰가 
                Text("Timer count = \\(timerData.timeCount)")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding()
                
                Button(action: resetCount) {
                    Text("Reset Counter")
                }
                
                NavigationLink(destination: SecondView()) {
                    Text("Next Screen")
                }.padding()
                
            }
            .padding()
        }
    }
    
    func resetCount() {
        timerData.resetCount()
    }
}

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

 

  • SecondView.swift
import SwiftUI

struct SecondView: View {
    
    @EnvironmentObject var timerData: TimerData
    
    var body: some View {
        VStack {
            Text("Second View")
                .font(.largeTitle)
            Text("Timer Count = \\(timerData.timeCount)")
                .font(.headline)
        }
        .padding()
    }
}

struct SecondView_Previews: PreviewProvider {
    static var previews: some View {
        SecondView().environmentObject(TimerData())
    }
}
  • EnvironmentObject를 쓰면 하위뷰로 참조체를 전달하지 않아도 동일한 데이터에 접근할 수 있다.
  • 단, 앱을 실행하는 최초 파일 (프로젝트 이름과 같은 swift파일)에서 environmentObejct(인스턴스)를 작성해줘야 동일한 environmentObejct를 사용할 수 있다.
    • 프리뷰 구조체에서 적어주는 것과 같다.

 

 

 


 

 

📒 Observable Object Demo

팀원들과 함께 진행해보기
  • Timer
    • objc로 만들어져 있다.

 

정리

✔️ 단순하게 한 화면에서 입력 값을 받아 처리할 거라면 State
✔️ 이어지는 화면에 걸쳐서 넘기고 영향을 받아야 한다면 Observable
✔️ 여러 화면, 거의 모든 화면에 걸치는 데이터라면 Environment

SwiftUI 안에 Combine이 포함되어 있을 가능성이 높다. 그래도 써주자~

 

데모

  • TimerData.swift
import Foundation
import Combine

class TimerData: ObservableObject {
    @Published var timeCount = 0
    var timer: Timer?
    
    init() {
        timer = Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(timerDidFire),
            userInfo: nil,
            repeats: true
        )
    }
    
    @objc func timerDidFire() {
        timeCount += 1
    }
    
    func resetCount() {
        timeCount = 0
    }
    
}
  • ContentView.swift (FirstView)
struct ContentView: View {
    @EnvironmentObject var timerData: TimerData
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Timer count = \\(timerData.timeCount)")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding()
                
                Button(action: resetCount) {
                    Text("Reset Counter")
                }
                
                NavigationLink(destination: SecondView()) {
                    Text("Next Screen")
                }.padding()
                
            }
            .padding()
        }
    }
    
    func resetCount() {
        timerData.resetCount()
    }
}

 

  • SecondView.swift
struct SecondView: View {
    
    @EnvironmentObject var timerData: TimerData
    
    var body: some View {
        VStack {
            Text("Second View")
                .font(.largeTitle)
            Text("Timer Count = \\(timerData.timeCount)")
                .font(.headline)
        }
        .padding()
    }
}

 

  • ObservableDemoApp.swift (메인앱 파일)
@main
struct ObservableDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(TimerData())
        }
    }
}

ContentView로 TimerData() 객체를 전달해준다.

 

 

📌 코드 수행 과정

  1. 앱이 처음 열리면 ContentView에 environment 객체로 TimerData가 생성된다.
  2. TimerData는 environment 객체이므로 뷰와 뷰 사이로 전달해줄 필요가 없다..!
  3. ContentView와 SecondeView에서 사용되는 Timer 객체는 모두 같다. 그러므로 타이머도 똑같이 돌아간다!

 

📌 시연 영상

 

  Comments,     Trackbacks