SwiftUI는 함수형 프로그래밍이 대중화된 이후에 등장했기 때문에 함수형의 개념을 많이 차용해서 사용했는데요. 오늘 알아볼 Observable Object가 그 좋은 예라고 볼 수 있을 것 같습니다.
Observable은 Rx를 공부했던 분에게는 어느 정도 익숙한 용어일텐데요. 바로 데이터를 발행하는 객체를 의미하죠. SwiftUI에서도 같은 뜻입니다. View를 위한 데이터를 발행하는 객체를 의미합니다.
더 간단하게 이야기하자면 @State의 객체 버전이라고 생각하면 쉽습니다. SwiftUI는 @State 변수와 마찬가지로 Observable Object를 관찰하고 있다가 데이터가 바뀌면 UI를 변경합니다.
요즘 기름 값이 많이 올랐죠? 한번 기름 값을 마음대로 올리고 내리는 앱을 만들어 봅시다.
먼저 Observable Object 먼저 구현해보겠습니다.
Observable Object로 선언하고자 하는 객체는 ObservableObject를 상속 받으면 됩니다. 간단하죠?
추가적으로 아래 코드를 보시면 Property Wrapper인 @Published를 붙여놓은 것을 볼 수 있는데요. 말 그대로 해당 변수의 값을 발행할 것이라는 뜻입니다. 다시 말하면 해당 property의 값을 변경할 때 UI도 변경하고자 할 때 붙입니다.
import SwiftUI
class Oil: ObservableObject {
@Published var isGas = true //👉 휘발유면 true, 경유면 false
@Published var price = 1000
}
Observable Object를 View에서 사용하기 위해서는 해당 객체에 Property Wrapper @ObservedObject를 붙여서 선언해야 합니다. View 입장에서 해당 객체를 관찰하고 객체는 관찰당하는 입장이니 “Observed”라는 표현이 적절하죠?
아래 코드를 보시면 객체의 상태를 표시하는 텍스트와 객체의 상태를 변경하는 버튼 3가지가 있는데요.
텍스트는 oil 객체의 isGas boolean 값을 읽어와서 휘발유인지 경유인지 표시하고 price 값을 읽어와서 표시합니다.
그리고 버튼 3개는 각각 isGas 값을 toggle하는 버튼, 그리고 price를 100씩 올리거나 내릴 수 있는 버튼들입니다.
한번 실행해보겠습니다.
import SwiftUI
struct BlogView: View {
@ObservedObject var oil = Oil()
var body: some View {
VStack {
Text("\(oil.isGas ? "휘발유" : "경유") 값: \(oil.price)")
Button(action: {
oil.isGas.toggle()
}, label: {
Text("다른 기름")
})
Button(action: {
oil.price += 100
}, label: {
Text("100원 올리기")
})
Button(action: {
oil.price -= 100
}, label: {
Text("100원 내리기")
})
}
}
}
Oil 객체의 property인 isGas와 price 모두 @Published로 선언했기 때문에 값을 변경할 때 바로바로 View에 반영되는 것을 볼 수 있습니다.
그렇다면 이번에 아래처럼 isGas에 @Publish를 빼고 버튼을 눌러봅시다.
import SwiftUI
class Oil: ObservableObject {
var isGas = true
@Published var price = 1000
}
아무리 눌러도 휘발유와 경유가 toggle된 것이 View에 반영되지 않는 것을 볼 수 있습니다. (여전히 @Published가 붙어있는 price는 잘 반영됩니다.)
여기서 알 수 있듯이 아무리 ObservableObject로 선언된 객체라고 하더라도 @Published로 선언된 property만이 View에 바로바로 반영될 수 있습니다.
@Published를 붙이지 않은 property라도 변경 되었을 때 절대로 반영되지 않는 것은 아닙니다. Observable Object는 뷰에 반영될 때 객체 단위로 반영됩니다.
쉬운 말로 하면 @Published가 붙은 객체가 변하면 View는 해당 property 값만 따로 읽어서 반영하는 것이 아니라 전체 Observable Object를 다시 View에 반영합니다.
아래 예시 실행 화면은 isGas에 @Published를 붙이지 않은 위 상황과 동일한 코드를 실행한 것입니다.
기름 종류를 바꿀 때는 View에 반영되지 않지만 가격을 올리거나 내리면 기름 종류가 View에 반영되는 것을 볼 수 있습니다. 다시 말하면 @Published로 선언한 price 값이 변경될 때 @Published로 선언하지 않은 isGas 값이 View에 반영되고 있습니다.
위 값은 현상이 나타나는 이유는 isGas 값이 바뀔 때는 View에서 Observable Object의 상태가 변경된 것을 몰랐고 price 값이 변경될 때 Observable Object의 변경된 모든 상태가 한번에 반영되기 때문입니다.
@Published가 값이 변경되자마자 자동으로 View에 알려줘서 편하기는 합니다만, 때로는 바로바로 반영하지 않고 특정한 조건을 만족했을 때만 View를 변경하고 싶을 때도 있습니다.
예를 들어 어떤 주유소에서는 100원 단위로 가격을 반영하지 않고 500원 단위로만 가격을 올리고 내린다고 생각을 해봅시다. 그렇다면 price 값이 500으로 나누어 떨어졌을 때만 View에 반영하면 됩니다.
아래처럼 willSet과 objectWillChange를 합쳐서 사용하면 됩니다. objectWillChange는 객체가 바뀐다는 것을 View에 알려주는 역할을 하는 객체입니다. 해당 객체의 send() 메소드를 이용해서 View에 수동으로 price가 바뀌었다는 것을 전달합니다.
class Oil: ObservableObject {
@Published var isGas = true
var price = 1000 {
willSet {
if newValue % 500 == 0 {
objectWillChange.send()
}
}
}
}
이렇게 하면 100원 올리기를 5번 클릭해서 price가 1500이 되었을 때 (= 500으로 나누어 떨어질 때) View에 있는 가격이 변경되는 것을 볼 수 있습니다. (내리는 것도 마찬가지!)