여러 View 에서 사용될 State 를 한데 저장해 놓고 쓸 수 있는 기능.
사용자 인터페이스 밖에 있으며 앱 내의 SwiftUI 뷰 구조체의 하위 뷰에만 필요한 데이터는 Observable 오브젝트를 이용한다.
사용자 인터페이스 밖에 있으며 여러 뷰에서 접근해야 하는 데이터는 Environmet 오브젝트를 활용한다.
매 초마다 숫자가 1씩 늘어나는 어플리케이션을 만들어보자. 하지만 이 어플리케이션은 예제를 위한 것 답게 쓸데없이, 똑같은 숫자를 보여주는 페이지 뷰가 두개다(ContentView, SecondView).
처음 뷰에서 다른 뷰로 넘어가도 같은 숫자가 나와야 한다.
그러기 위해선 Timer 오브젝트를 하나 만들고, 두개의 View 가 같은 오브젝트를 참조하게 만들어야 한다.
그럼 Xcode 를 이용해서 만들어 보자.
Xcode 에서 SwiftUI 로 SingleViewApp 템플릿을 골라 시작.
Xcode가 기본적으로 생성되는 template 파일에 더해, new file 로 SecondView.swift 와 TimerData.swift 파일을 만들자.
다음은 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 = timeCount+1
}
func resetCount(){
timeCount=0
}
}
ObservableObject 프로토콜을 따르는 클래스 TimerData를 만든다. ObservableObject 프로토콜은 클래스에만 쓰일 수 있다.
TimerData 외부에서 참조할 변수 timeCount 를 @Published 어노테이션으로 지정해 준다.
다음은 각각 ContentView.swift 와 SecondView.swift 의 코드이다.
import SwiftUI
struct ContentView: View {
@ObservedObject var timerData: TimerData = TimerData()
var body: some View {
NavigationView {
VStack{
Text("Timer Count: \(timerData.timeCount)").font(.largeTitle).fontWeight(.bold).padding()
Button(action: timerData.resetCount){
Text("Reset Counter")
}
NavigationLink(destination: SecondView(timerData: timerData)){
Text("To the Second View")
}.padding()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(timerData: TimerData())
}
}
import SwiftUI
struct SecondView: View {
@ObservedObject var timerData: TimerData
var body: some View {
NavigationView {
VStack{
Text("Second View Timer Count: \(timerData.timeCount)").font(.largeTitle).fontWeight(.bold).padding()
Button(action: timerData.resetCount){
Text("Reset Counter")
}
}
}
}
}
둘다 코드에 "import TimerData" 같은 내용이 없다. TimerData 클래스가 해당 Xcode 프로젝트 안에 포함돼 있으면, 따로 import 할 필요없이 Xcode 컴파일러가 알아서 처리해준다.
(네임 스페이스가 프로젝트 전범위라고도 볼 수 있는건가? 이 부분은 나도 안찾아봐서 모르겠다)
결과물은 다음과 같다.
여기서 To the Second View 를 클릭하면 아래와 같은 View가 나타난다.
첫번째 뷰에서 두번째 뷰로 넘어가도, 같은 카운트를 공유하고 있음을 볼 수 있다.
첫번째 뷰에서 @ObservedObject var timerData: TimerData = TimerData() 를 통해 ObservableObject 를 불러오고, NavigationLink(destination: SecondView(timerData: timerData)) 를 통해 SecondView 에 timerData 오브젝트를 전달해 주었다.
timerData 오브젝트를 전달받은 두번째 뷰는 첫번째 뷰와 같은 오브젝트를 참조하고 있는 것이다.
Observable 오브젝트를 Environment 오브젝트로 변환하면, 첫번째 뷰에서 두번째 뷰로 참조체를 전달할 필요없이 모든 뷰에서 Environment 오브젝트에 접근이 가능하다.
TimerData.swift 파일은 수정할 필요 없이, ContentView와 SecondView, 그리고 SceneDelegate 파일만 수정하면 된다.
ContentView 과 SecondView 의 수정사항은 다음과 같다.
import SwiftUI
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: timerData.resetCount){
Text("Reset Counter")
}
NavigationLink(destination: SecondView()){
Text("To the Second View")
}.padding()
}
}
}
}
import SwiftUI
struct SecondView: View {
@EnvironmentObject var timerData: TimerData
var body: some View {
NavigationView {
VStack{
Text("Second View Timer Count: \(timerData.timeCount)").font(.largeTitle).fontWeight(.bold).padding()
Button(action: timerData.resetCount){
Text("Reset Counter")
}
}
}
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(TimerData())
}
}
여기에 더해 SceneDelegate 파일을 수정해야한다. 루트화면이 생성될 때 TimerData 오브젝트가 Environment 오브젝트에 추가되도록 한다.
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
let timerData = TimerData()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(timerData))
self.window = window
window.makeKeyAndVisible()
}
...
}