cchanmi
[iOS] SwiftUI의 Property Wrapper 본문
최근 UIKit 프로젝트에 SwiftUI를 도입하게 되면서 접하게 된 Propery Wrapper에 대해 정리해 보려고 합니다.
@State
A property wrapper type that can read and write a value managed by SwiftUI.
SwiftUI가 관리하는 값을 읽고 쓸 수 있는 property wrapper입니다.
- @State는 View에서 초기값을 생성해야 하고, 다른 객체로부터 상태를 수정하는 것을 방지하기 위해 private으로 선언하는 것을 권장합니다.
- 값이 변경되면 변화를 감지하고, UI를 자동으로 다시 그리도록 합니다.
- 데이터를 전달할 때는 Binding<T>를 통해 @State 프로퍼티를 전달할 수 있습니다.
struct PlayButton: View {
@State private var isPlaying: Bool = false // Create the state.
var body: some View {
Button(isPlaying ? "Pause" : "Play") { // Read the state.
isPlaying.toggle() // Write the state.
}
}
}
@State 사용 예시
저 같은 경우는 이번에 SwiftUI에서 동작할 토스트 메시지를 만드는 과정에서 @State를 사용했습니다.
현재 기획 요구 사항에 맞게 토스트 메시지가 잠시 동안 보여지고 사라지는 애니메이션을 구현해야 했는데요. 변화하는 opacity 값에 따라 동작하도록 구현했습니다.
struct SmemeToastView: View {
@Binding var type: SmeemToast?
@State private var opacity: Double = 0.0
var body: some View {
if let toast = type {
ZStack {
...
.opacity(opacity)
.transition(.opacity)
.onAppear {
withAnimation(.easeIn(duration: 0.6)) {
opacity = 1.0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation(.easeOut(duration: 0.6)) {
opacity = 0.0
type = nil
}
}
}
}
}
}
}
@Binding
A property wrapper type that can read and write a value owned by a source of truth.
source of truth가 소유한 값을 읽고 쓸 수 있는 속성 래퍼라고 되어 있는데,
쉽게 이야기하면 이름과 같이 두 개의 뷰간의 데이터 바인딩을 할 때 사용됩니다.
struct PlayerView: View {
var episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
.foregroundStyle(isPlaying ? .primary : .secondary)
PlayButton(isPlaying: $isPlaying) // Pass a binding.
}
}
}
struct PlayButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
PlayerView에서 @State를 초기화해서 들고 있으며, 이를 $로 전달하여 PlayButton의 @Binding 프로퍼티와 연결되어 이 변화를 Playeriew의 Text와 PlayButton 모두에 반영합니다.
@ObservedObject
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
observable 객체를 구독하는 property wrapper
즉, 관찰 가능한 객체를 구독하고 관찰 가능한 객체가 변경될 때마다 UI를 업데이트 시켜주는 역할을 합니다.
ObjservedObject를 채택하면 해당 객체는 관찰 가능한 객체가 됩니다.
class DataModel: ObservableObject {
@Published var name = "Some Name"
@Published var isEnabled = false
}
struct MyView: View {
@StateObject private var model = DataModel()
var body: some View {
Text(model.name)
MySubView(model: model)
}
}
DataModel이 ObservableObject를 채택하여 관찰 가능한 객체가 되었고, Publisher 프로퍼터 래퍼인 값이 변경된다면, 이를 지켜보고 있는 MyView에서 새로운 View를 refresh 하게 됩니다.
@StateObject
A property wrapper type that instantiates an observable object.
observable object를 채택한 객체를 인스턴스화하는 프로퍼티 래퍼입니다.
처음에 ObservedObject와 StateObject의 차이가 무엇인지에 대해 궁금했었는데요.StateObject 같은 경우 화면 구조가 재생성되어도 뷰를 다시 만들지 않고 동일한 뷰를 사용한다는 특징이 있습니다. 즉, 효율적인 측면에서 이점이 있다고 말할 수 있습니다.
@StateObject를 사용하는 View는 내부적으로 ObservableObject를 만드는데, 이 과정에서 SwiftUI는 @StateObject와 관련된 인스턴스를 하나만 생성하고 View가 초기화될 때 재사용합니다.
그럼 어떨 때 @ObservedObject, @StateObject를 사용해야 할까?
@ObservedObjecct는 상태 변화 후 ->View 다시 생성@
StateObject는 상태 변화후 -> 동일한 View 사용
만약 어떠한 프로퍼티를 자식 뷰에게도 주입시켜야 한다면, 부모 뷰와 자식 뷰의 생명주기가 같아야 하기 때문에 그런 경우는 @ObjservedObject를 사용.기본적으로는 @StateObject를 사용하는 것이 성능상이나, 디버깅 측면에서 좋을 것 같다는 생각이 듭니다.
@EnvironmentObject
A property wrapper type for an observable object that a parent or ancestor view supplies.
상위 뷰가 제공하는 관찰 가능한 객체이며, subView들이 접근 가능한 프로퍼티 래퍼입니다.
쉽게 말해서 전역적으로 데이터를 공유하는 매커니즘이라고 볼 수 있습니다.
@EnvironmentObject 사용법은 이러합니다.
1. 관찰될 객체가 ObservableObject를 구독
2. 이를 상위뷰에서 @Environment로 설정
3. 하위뷰에서 environmentObject modifier를 사용하여 감싸줌
@EnvironmentObject를 사용하기 위해서는 상위뷰가 ObservableObject를 구독하고,
이를 @Environment로 설정하고,
그 하위 뷰에서 해당 객체를 .environmentObject로 감싸주면 전역 변수처럼 접근할 수 있다는 것입니다.
class DataModel: ObservableObject {
@Published var name = "Some Name"
@Published var isEnabled = false
}
struct MyView: View {
@StateObject private var model = DataModel()
var body: some View {
Text(model.name)
MySubView()
.environmentObject(model)
}
}
struct MySubView: View {
@EnvironmentObject var model: DataModel
var body: some View {
Toggle("Enabled", isOn: $model.isEnabled)
}
}
주의해야 할 점은 @EnvironmentObject에 값이 없을 경우 접근하게 되면 crash가 날 수 있다는 점입니다.
@Environment
A property wrapper that reads a value from a view’s environment.
뷰 환경에서 값을 읽는 속성 래퍼입니다. 키 경로를 사용하여 읽을 값을 나타냅니다.
@EnvironmentObject와 다른점은 View의 환경변수를 읽는 데 사용된다는 점입니다.
struct ContentView : View {
@Environment(\.presentationMode) private var presentationMode
var body: some View {
if presentationMode.isPresented {
return Text("닫기")
} else {
return Text("뒤로 가기")
}
}
}
presentation mode에 따라 다른 화면을 그리고 싶은 경우 다음과 같이 활용할 수 있습니다.coodinator patten이 생각이 나네요
추가로 environment에 custom property를 추가할 수도 있다고 하네요.
@AppStorage
A property wrapper type that reflects a value from UserDefaults and invalidates a view on a change in value in that user default.
UserDefaults에 대한 프로퍼티 래퍼입니다. UserDefaults의 값이 변화하면 UI를 다시 그리게 됩니다.
struct ContentView: View {
@AppStorage("lastTap") var lastTap: Double?
var dateString: String {
if let timestamp = lastTap {
return Date(timeIntervalSince1970: timestamp).formatted()
} else {
return "Never"
}
}
var body: some View {
Text("Button was last clicked on \(dateString)")
Button("Click me") {
lastTap = Date().timeIntervalSince1970
}
}
}
여러 프로퍼티 래퍼에 대해 알아보았는데요.
해당 프로퍼티가 어떤 타입인지, 하나의 데이터 모델을 가지는지, 전역으로 관리하는지 등...
상황에 맞게 정확하게 프로퍼티 래퍼를 사용하는 것이 좋을 것 같습니다!
'iOS' 카테고리의 다른 글
[iOS] Github Action 도입기 - build & test 과정에서 겪은 error 모음 (1) | 2024.12.01 |
---|---|
[iOS] Github Action 도입기 - build & test (2) | 2024.11.30 |
[RxSwift] RxSwift의 메모리 누수에 대해서 (1) | 2024.09.08 |
[iOS] UIKit+Combine 환경에서 CollectionView Cell 바인딩 문제 해결 (1) | 2024.09.07 |
[iOS] RxSwift 정리 (1) | 2024.09.04 |