본문 바로가기

TIL

[TIL] @AppStorage Custom Property Wrapper로 전환기

적용 동기

아래 사진에서 빨간 박스로 표시되어 있는 것처럼 앱 로그인 시 프로필 이미지를 @AppStorage 프로퍼티 래퍼를 통해 UserDefaults에 저장하고 있습니다.

 

하지만 프로필 이미지도 유저의 개인정보로 취급될 수 있다고 생각하여 Token과 마찬가지로 Keychain에 저장해보기로 하였습니다.

 

Property Wrapper란?

Custom Property Wrapper를 만들기 앞서 Property Wrapper에 대해 간단히 알아보겠습니다.

 

Property Wrapper란 Swift 5.1부터 도입된 개념이며,  Swift Programming Language Guide에 따르는 정의는 다음과 같다고 합니다.

A property wrapper adds a layer of separation between the code that manages how a property is stored and the code that defines a property. -swift.org

 

위 정의에서 알 수 있듯, Property Wrapper 새로운 로직을 래핑하는 타입입니다.

더 간단히 말하면, Property Wrapper는 getter, setter 메서드 동작 구현 및 재사용할 수 있는 사용자 지정 유형(a custom type)을 정의할 수 있는 기능을 말한다고 합니다.

 

Property Wrapper의 구조 파악

자 이제 정의를 살펴보았으니 Property Wrapper의 기본 구조에 대해 파악해보도록 해보겠습니다.

 

@State, @Binding, @StateObject, @ObservedObject, @Environment, @EnvironmentObject, @AppStorage 등 많은 Swift built-in Property Wrapper들이 존재하지만 그중 가장 대표적인 @State의 구조에 대해 살펴보겠습니다.

 

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {

    public init(wrappedValue value: Value)

    public init(initialValue value: Value)

    public var wrappedValue: Value { get nonmutating set }

    public var projectedValue: Binding<Value> { get }
}

@propertyWrapper

 Property Wrapper를 정의하기 위한 필수 Attribute입니다.

DynamicProperty

DynamicProperty는 update()라는 메소드가 있고, 내부적으로 이 메소드가 불리며 뷰를 업데이트 시키는데 사용되는 프로토콜입니다.

기본 구조는 아래와 같습니다.

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol DynamicProperty {
    mutating func update()
}

주로 propertyWrapper와 같이 사용되며, 특정 값이 변경될 때 뷰를 update() 되로록 구현됩니다.

wrappedValue

Property Wrapper에서 실질적으로 값을 저장하고, 처리하는 연산 프로퍼티(Computed Property)입니다.

getter와 setter 메서드를 통해 Property Wrapper가 Property에 사용자 지정 동작(a Custom Behavior)을 추가할 수 있습니다.

nonmutating 키워드

wrappedValue 연산 프로퍼티의 setter 메서드를 보시면 앞에 nonmutating이라는 키워드를 찾아보실 수 있습니다.

이 키워드를 가진 setter 메서드 즉, nonmutating set은 Value Type 내부를 변경할 수 없다는 의미입니다.

주로 nonmutating set 안에서 self 관련된 프로퍼티가 아닌 다른 프로퍼티를 변경하는 경우 사용됩니다.

@propertyWrapper
struct Counter: DynamicProperty {
  @State private var value = 0
  
  var wrappedValue: Int {
    get { return value }
    nonmutating set { value = newValue }
  }
}

projectedValue

propertyWrapper 내부에서 다른 값을 정의하여 사용하는쪽에서 달러 `$` 키워드로 해당 값에 접근할 수 있으며, 주로 propertyWrapper에서 부가적인 프로퍼티 접근할 때 사용됩니다.

 

더구나 @Binding과 관련이 깊은 변수입니다.

Custom Property Wrapper 정의시 Binding 정의하여 다른 뷰와의 양방향 바인딩을 정의할 때도 사용됩니다.

@propertyWrapper
struct Flag {
  let name: String
  
  var wrappedValue = false
  var projectedValue: String { self.name }
  
  init(name: String) {
    self.name = name
  }
}

// 사용
class ViewController: UIViewController {
  @Flag(name: "isSignIn") var isSignIn
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    print(self.isSignIn) // false
    print(self.$isSignIn) // "isSignIn"
  }
}

 

Custom Property Wrapper

다시 원점으로 돌아와 설명해보겠습니다. 

본래 목적처럼 프로필 이미지를 Keychain에 저장하려면 단순히 기존 정의되어 있던 KeychainStorage에 wrappedValue 프로퍼티를 사용하면 되지만 하위 뷰와의 양방향 바인딩도 필요하기 때문에 추가적으로 projectedValue를 KeychainStorage에 새로 정의 해주어야 합니다.

ProjectedValue 정의 

사실 위의 경우 단순히 기존에 정의되어 있던 wrappedValue 프로퍼티를 이용하면 projectedValue를 간단히 구현할 수 있습니다.

구현 내용은 다음과 같습니다.

var projectedValue: Binding<String> {
    Binding(get: {
        wrappedValue
    }, set: {
        wrappedValue = $0
    })
}

 

위 코드를 보시면 projectedValue를 외부에서 접근할 땐, wrappedValue 프로퍼티를 반환하고, 새로 작성될 땐, 그 새로운 값을 wrappedValue에 할당하는 것을 알 수 있습니다. 이처럼 wrappedValue를 통해 간단히 구현할 수 있습니다.

 

마무리

긴글 읽어주서서 감사합니다.

 

참고 사이트

https://ios-development.tistory.com/895 

https://ios-development.tistory.com/1163

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers

https://medium.com/@EvangelistApps/property-wrappers-in-swift-51cee87e2c32

https://youtu.be/2wzq6SQkSJE?si=dm4bdEreqGeGO2G8

https://velog.io/@valse/SwiftUI-%EB%8C%80%EC%B2%B4-%EA%B0%92%EC%9D%B4-%EC%99%9C-%EC%95%88-%EB%B3%80%ED%95%98%EB%8A%94-%EA%B1%B4%EB%8D%B0Property-Wrappers