Property Wrappers 에 대해 알아보자 - State, Binding ①

2023. 11. 4. 00:16Mobile/SwiftUI

SwiftUI 에서 Property Wrapper들의 종류가 적지 않은 걸로 안다. 앞에는 주로 @ 표시를 붙이고 사용하는데, 그 종류를 알아보기 앞서 Property Wrapper 가 가지는 의미에 대해서 알아봐야겠다. 

 

Swift 내에서 프로퍼티가 가지는 속성과 역할을 보면, 

Properties
- Access stored and computed values that are part of an instance or type. 
저장되거나 계산되는 인스턴스 또는 타입의 일부인 value(값)에 액세스(접근) 한다. 

 

그렇다면, 속성을 감싸서 사용할 때 감싼 걸 가지고 와서 사용할 것 같은 프로퍼티 래퍼 이 녀석은

 

Property Wrapper 
A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.
속성 저장 방법을 관리하는 코드, 속성을 정의하는 코드사이에 분리 계층을 추가해준다. 
➡  특정 값을 특정 목적으로 변환하도록 한 번 감싸는 역할

 

여기까지는 모두 Swift 문법 내에서의 Property Wrapper 의 정의다.

1. SwiftUI 프로퍼티 래퍼를 사용하면 각 뷰가 변경 가능한 데이터와 상호 작용하는 방법을 선언할 수 있다.

2. SwiftUI 는 속성의 저장을 관리한다. 값이 변경되면 SwiftUI는 값에 따라 뷰 계층 구조의 일부를 업데이트한다. 상태의 기본 값에 액세스(접근)하려면 해당 속성을 사용한다. 

 

 

@State

@frozen @propertyWrapper 
struct State<Value>

SwiftUI 에서 관리하는 값을 읽고 쓸 수 있는 Property Wrapper 유형이다.

 

여기서 잠깐! 알아야할 게 참 많구나..

@frozen 이 붙었는데 이 놈은 대체 뭔가? 얼려진?  '동결된' 이 그나마 받아들이기 쉬울 것 같다. 

이 녀석은 구조체(struct), 열거형(enum) 선언에 적용되어 타입 변경을 제한하는 역할을 한다고 한다.

( 이 속성은 library evolution mode 1) 에서 컴파일할 때만 허용한다고 한다.) 

 

> NOTE

library evolution mode
Swift 에서 라이브러리 개발자가 API 에 추가적인 변경을 하더라도 이전 버전과 바이너리 호환성을 유지할 수 있게 해 주는 기능이다. Swift 5.1 에 도입되었으며, 이를 통해 서로 다른 컴파일러 버전으로 빌드된 Swift 모듈들이 하나의 앱에서 함께 사용할 수 있다고 한다. 단 이 모드는 기본적으로 비활성화 되어 있다. 


뷰 계층 구조에 저장하는 지정된 값 유형에 대한 단일 정보 소스로 상태를 사용.

@State 프로퍼티 래퍼는 View 개체 내부에서 사용되며 뷰가 @State 에 대한 모든 변경 사항에 응답할 수 있도록 한다.
속성 선언에 @State 속성을 적용하고 초기 값을 제공하여 앱, 장면 또는 뷰에서 상태 값을 생성한다. 

 

> NOTE

SwiftUI 가 제공하는 스토리지 관리와 충돌할 수 있는 memberwise initialize에서 상태를 설정하는 것을 방지하려면 State를 비공개로 선언해서 사용하는 것이 좋다.

* memberwise initialize (멤버와이즈 초기화)

프로퍼티를 보통 멤버 변수라고 부르는 까닭에, 이 초기화 구문을 멤버와이즈 초기화 구문(Memberwise Initializer)라고 부르기도 함.

더보기

코드 출처: 야곰 프로그래밍 5.6 3판 

struct Resolution {
    var width = 0
    var height = 0

	func desc() -> String {
		return "Return 구조체"
    }
}

class VideoMode {
    var interlaced = false
    var frameRate = 0.0
    var name: String

    func desc() -> String {
    	return "VideoMode 클래스"
    }
}

/// memberwise initializer

//width 와 height 를 매개변수로 하여 Resolution 인스턴스를 생성
let defaultRes = Resolution(width: 1024, height: 768)
// Resolution(width: Int, height: Int) 모든 프로퍼티의 초기값을 입력받는 
// 멤버와이즈 초기화 구문. 내부적으로 모든 프로퍼티를 초기화함

 

struct PlayButton: View {
	// private 하게 상태를 지정하고 그 타입은 Bool 값으로 지정해서 생성
    @State private var isPlaying: Bool = false
	
	var body: some View {
        Button(isPlaying ? "Pause" : "Play") { // State 를 읽고
            isPlaying.toggle() // 사용하는 방법
        }
    }
	
}

 

상태 인스턴스를 직접 참조해 래핑된 값 -> isPlaying 에 액세스 할 수 있음. 값을 읽고, 쓰는 것이 가능

 

Share state with subviews - @Binding

(하위 뷰와 상태를 공유하기)

 

언어적인 의미로는 Bind - 묶다, Binding - 제본의 뜻이 있다. 

일반적으로 프로그래밍에서도 언어 바인딩이 있는데, 하나를 다른 것으로 매핑시키는 것으로 의미한다고 한다.

 

상태 속성을 하위 뷰 전달하면 SwiftUI는 Container view 에서 값이 변경될 때마다 하위 뷰를 업데이트 할 수 있다.

그러나, 하위 뷰는 값을 수정할 수 없다!

 

하위 뷰가 State 가 저장된 값을 수정할 수 있도록 하려면 Binding 을 전달해 수정이 가능해진다! 

1. 상태를 선언하고 ( @State private var isPlaying: Bool = false )

2. 상태의 projectedValue 에 접근(access) 해 상태 값에 대한 바인딩을 가져오고 사용시에 $ 를 붙여 가져온다

    -> projectedValue : 바인딩의 상태 값 

var projectedValue: Binding<Value> { get }

 

 

struct PlayerView: View {
    @State private var isPlaying: Bool = false // Create the state here now.


    var body: some View {
        VStack {
            PlayButton(isPlaying: $isPlaying) // Pass a binding.


            // ...
        }
    }
}

 


Store observable objects 

observable 의 사전적의미는

주목할 만한, 지켜야 할, 관찰할 수 있는 이므로 Observable 'object(객체)' 는 
관찰가능한 객체가 받아들이고 사용하기에 좋을 것 같다.

Observable() 매크로를 사용해 생성한 관찰 가능한 개체를 State 에 저장할 수 있다.

Observable() : Observable 프로토콜의 적합성을 정의하고 구현한다.

@attached(member, names: named(_$observationRegistrar),named(access), named(withMutation)) 
@attached(memberAttribute) @attached(extension, conformances: Observable) 
macro Observable()

 

Observable

@Observable
class Book {
    var title = "A sample book"
    var isAvailable = true
}

struct ContentView: View {
    @State private var book = Book()

    var body: some View {
        BookView(book: book)
    }
}

struct BookView: View {
    var book: Book

    var body: some View {
        Text(book.title)
    }
}

상태 속성은 해당 값에 대한 바인딩을 제공한다. 객체를 저장할 때 해당 객체에 대한 바인딩, 특히 객체에 대한 참조를 얻을 수 있다. 이는 참조를 nil 로 설정하는 등 다른 하위 뷰의 상태에 저장된 참조를 변경해야 할 때 유용하다.

struct ContentView: View {
    @State private var book: Book?

    var body: some View {
        DeleteBookView(book: $book)
            .task {
                book = Book()
        }
    }
}

struct DeleteBookView: View {
    @Binding var book: Book?

    var body: some View {
        Button("Delete book") {
            book = nil
        }
    }
}

그러나 해당 개체의 속성을 변경해야 하는 경우 State 에 저장된 개체에 바인딩을 전달할 필요는 없다.

struct ContentView: View {
    @State private var book = Book()

    var body: some View {
        BookCheckoutView(book: book)
    }
}

struct BookCheckoutView: View {
    var book: Book

    var body: some View {
        Button(book.isAvailable ? "Check out book" : "Return book") {
            book.isAvailable.toggle()
        }
    }
}

개체의 특정 속성에 대한 바인딩이 필요한 경우 바인딩을 개체에 전달하고 필요한 경우 특정 속성에 대한 바인딩을 추출한다. 또는 개체 참조를 전달하고 Bindable 프로퍼티 래퍼를 사용해 특정 속성에 대한 바인딩을 만든다.

예를 들어 다음 코드에서 BookEditorView는 book을 @Bindable 로 래핑한다. 그런 다음 뷰는 $ 구문을 사용해 제목에 대한 바인딩을 TextField 에 전달한다.

struct ContentView: View {
    @State private var book = Book()

    var body: some View {
        BookView(book: book)
    }
}

struct BookView: View {
    let book: Book

    var body: some View {
        BookEditorView(book: book)
    }
}

struct BookEditorView: View {
    @Bindable var book: Book

    var body: some View {
        TextField("Title", text: $book.title)
    }
}

https://developer.apple.com/documentation/swift/applying-macros/

 

 

매크로의 종류라니,, 파본 적이 없던 분류인데, 이건 나중에 기억해뒀다가 다시 다뤄봐야겠다!  무튼 이 매크로는 사용자 정의 타입에 관찰지원을 추가하고 해당 유형을 Observable 프로토콜을 따르게 한다. 

 

+ ) Macros 추가 
https://playground-coding.tistory.com/81

 

Macro 에 대해서 알아보자. ( 수정중 )

https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation Update your existing app to levera

playground-coding.tistory.com

 

https://developer.apple.com/documentation/swiftui/state/


 

State | Apple Developer Documentation

A property wrapper type that can read and write a value managed by SwiftUI.

developer.apple.com

 

결과적으로 이번 시간에 정리한 State 와 Binding 은 뷰의 업데이트를 관리하는 데 사용되는 것들이다.

 

생각보다 State 와 연관된 게 많은데, 이건 나눠서 Observable 과 Observable Object 를 묶어서 탐구해봐야겠다.