[Swift 문법] 프로퍼티 래퍼(Property Wrapper)

Yellowtoast·2024년 1월 14일
0

Swift

목록 보기
11/11
post-thumbnail

해당 글은 iOS 스터디를 위한 기본 문법을 정리한 글 입니다.
Advanced Swift (by Chris Eidhof) 책의 내용을 참고하여 작성하였습니다.

Property Wrappers

Swift에 프로퍼티 래퍼를 추가하게 된 중요한 동기 중 하나는 SwiftUI의 출시입니다.

사실 프로퍼티 래퍼의 장점은, 컴파일러에 내장된 기능을 사용하지 않고 라이브러리처럼 lazy property 를 작성할 수 있다는 점입니다.

프로퍼티 래퍼를 사용하면 프로퍼티 선언의 동작을 수정할 수 있습니다.

struct Toggle: View { 
	@Binding var isOn: Bool 
	// ...
    var body: some View {
      if isOn { 
      // ...
      }
     }
    }

위 코드에서 @Binding은 속성 래퍼입니다. 일반 Bool 프로퍼티를 사용할 때와 마찬가지로 isOn 프로퍼티를 사용하여 값을 가져오고 설정할 수 있습니다. 하지만 동작이 다릅니다.

  1. 메모리가 Toggle 값 외부에 저장되어 뷰에 불투명하게 표시됩니다. 2. 변경 메서드가 없이도 값을 수정할 수 있습니다. 프로퍼티 래퍼는 SwiftUI 전체에서 많이 사용되며, 주로 상태를 관리하는 데 사용됩니다.

클래스 및 구조체의 프로퍼티, 지역 변수(전역 변수 제외), 함수 인수에 프로퍼티 래퍼를 사용할 수 있습니다.

Property Wrapper의 사용

프로퍼티 래퍼의 또 다른 사용 사례는 프로퍼티로 인해 뷰의 일부가 무효화되는 경우가 많은 UIKit 및 AppKit을 사용할 때입니다. 이 장의 앞부분에서 특정 프로퍼티가 변경될 때 변경 관찰자를 사용하여 뷰의 레이아웃을 무효화하는 코드를 기억해 보십시오:
아래는

class MyView: UIView {
@Invalidating(.layout) var pageSize: CGSize =
CGSize(width: 800, height: 600) }

@Invalidating 속성 래퍼는 내부에 크기를 저장하고, 크기가 변경될 때마다 사용자를 대신하여 setNeedsLayout을 호출합니다. 뷰의 여러 부분(레이아웃, 제약 조건, 표시 등)을 모두 무효화하는 프로퍼티가 많은 경우 이 프로퍼티 래퍼를 사용하면 코드를 상당히 깔끔하게 정리하는 데 도움이 될 수 있습니다. 일반 프로퍼티의 변경 관찰자처럼 작동하는 프로퍼티 래퍼와 함께 willSet 및 didSet을 계속 사용할 수 있습니다

뷰 외부에서도 프로퍼티 래퍼는 유용합니다. 커뮤니티에서 작성한 프로퍼티 래퍼에는 Codable을 통해 유형이 인코딩 또는 디코딩되는 방식을 사용자 정의하기 위한 프로퍼티 래퍼, UserDefaults를 둘러싼 프로퍼티 래퍼, 반응형 프로그래밍을 더 간단하게 만들기 위한 프로퍼티 래퍼(예: Combine의 @Published) 등이 있습니다.

@propertyWrapper
struct UserDefault<Value>{
    let key: String
    let defaultValue: Value
    let container: UserDefaults = .standard
    
    var wrappedValue: Value{
        get {
            container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }
    
}

extension UserDefaults{
    public enum Keys{
        static let hasOnboarded = "hasOnboarded"
    }
    
    @UserDefault(key: UserDefaults.Keys.hasOnboarded, defaultValue: false)
    static var hasOnboarded: Bool
}

왜 Property Wrapper를 사용하는가?

Property Wrapper는 공통적인 로직들을 프로퍼티 자체에 연결할 수 있어 보일러플레이트 코드와 코드 재사용성을 높혀준다고 소개되어있습니다.

“메서드 단위로 중복을 해결하는 방법은 클래스 혹은 구조체 내에서 접근을 해야하는데… 프로퍼티 단위로 중복이 발생하면, 더 작은 단위로 로직을 공통화할 수 없을까?"

이에 대한 해답이 Property Wrapper라고도 볼 수 있습니다.

사용법

프로퍼티 래퍼는 구문 기능으로, 프로퍼티 래퍼 없이 바인딩 및 무효화를 사용할 수 있습니다. 다음은 이전의 바인딩 예제이지만 속성 래퍼 없이 재작성된 예제입니다:

struct Toggle: View {
  var isOn: Binding<Bool> 
  	// ...
  var body: some View {
    if isOn.wrappedValue { 
    // ...
    } 
  }
}
@propertyWrapper 
class Box<A> {
	var wrappedValue: A
    
    init(wrappedValue: A) { 
    	self.wrappedValue = wrappedValue
	} 
}

struct Checkbox {
	@Box var isOn: Bool = false
    
	func didTap() { 
    	isOn.toggle()
	} 
 }
struct Checkbox {
	private var _isOn: Box<Bool> = Box(wrappedValue: false) 

  var isOn: Bool {
      get { _isOn.wrappedValue }
      nonmutating set { _isOn.wrappedValue = newValue } 
  }

  func didTap() { 
      isOn.toggle()
  } 
}

컴파일러는 프로퍼티 래퍼로 표시된 각 프로퍼티에 대해 밑줄이 앞에 붙은 실제 저장 프로퍼티를 생성합니다.

또한 기본 프로퍼티 래퍼의 wrappedValue에 액세스하는 계산된 프로퍼티가 생성됩니다. 프로퍼티가 값으로 초기화되는 경우(위의 예에서 isOn은 값 false로 초기화됨) 프로퍼티 래퍼는 .init(wrappedValue:)로 초기화됩니다.

프로퍼티 래퍼를 정의할 때는 최소한 wrappedValue에 대한 게터를 제공해야 합니다. 설정자는 선택 사항이며, 설정자의 존재 여부에 따라 계산된 프로퍼티에 대한 설정자가 생성됩니다. 위의 경우 설정자가 있으므로 계산된 프로퍼티에 대해서도 설정자가 생성됩니다. Box는 클래스이므로 생성된 설정자는 변하지 않습니다. 설정자와 마찬가지로 init(wrappedValue:) 역시 선택 사항이지만 Box에서 제공하므로 Box<Bool> 을 Bool로 초기화할 수 있습니다.

알아보면 좋을 점

  1. Class에서 쓰는 propertyWrapper와 Struct에서 쓰는 propertyWrapper의 차이점

  2. Struct는 값을 바꿀 수 없는데, 어떻게 Struct 로 선언된 @State는 바뀐 값을 알려줄 수 있나?

profile
Flutter App Developer

0개의 댓글