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 객체는 publisher와 subscriber 간의 관계를 쉽게 구축할 수 있게 한다.
그러므로 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() 객체를 전달해준다.
📌 코드 수행 과정
- 앱이 처음 열리면 ContentView에 environment 객체로 TimerData가 생성된다.
- TimerData는 environment 객체이므로 뷰와 뷰 사이로 전달해줄 필요가 없다..!
- ContentView와 SecondeView에서 사용되는 Timer 객체는 모두 같다. 그러므로 타이머도 똑같이 돌아간다!
📌 시연 영상
'외부활동 > 멋사 앱스쿨 1기' 카테고리의 다른 글
[SwiftUI] TabView, Context Menu를 사용해보자. (2) | 2022.12.09 |
---|---|
[SwiftUI] List, Navigation을 이용해서 간단한 앱 구현하기 (EVCar Demo) (0) | 2022.12.09 |
[TIL] 22-10-21: SwiftUI 기초(룰렛 게임) (2) | 2022.10.25 |
[TIL] 22-10-20: Typography, SwiftUI 기초(숫자 게임) (0) | 2022.10.25 |
[TIL] 22-10-19: SwiftUI 기초(Stack, Frame, State) (0) | 2022.10.24 |