[Swift] What does the prefix dollar $ sign mean: Property Wrapper

정유진·2022년 12월 28일
0

swift

목록 보기
15/25
post-custom-banner

Introduction 👩‍💻

Every time I try to hand over a state variable to another logic, I encounter the dollar sign ($) often; which makes me puzzled. I just guessed it means a binding value but it was a rough guess. I have tried to find a solid backup in apple's official documents to make my assumption convinced. Finally, the finding said Apple uses a prefix dollar sign intentionally when it comes to property wrapper.

Motivation 👏

@Published var date: Date = .now 
    
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    //publish timer every second
    Timer
        .publish(every: 1, on: .main, in: .default)
        .autoconnect()
        .assign(to: &$date) // assign to = republish to other publisher
        
    return true
}

The above code is using Timer object to get the time record every second so that our view could present the current time by subscribing published object at appDelegate. As Timer returns a publisher object by getting through publish method, assign method takes a published value and republishes it to another publisher. You can see the dollar preposition again at the method's first parameter.

I want to notify a changed value to a date property written at the first line so it can change the view's state. The first time, of course, I typed date (not $date) as a parameter. Here's a thing that occurred. It doesn't work as expected and shows this error below saying the variable date is not a type published. But my thought was 'yes it is! it has an annotation marked as published! why is a compiler saying it is not a type published? what's wrong with you?'

But no argue needed because the var date is a Date type variable as it is written down. The only difference between my date and a pure date is whether it has a @Published annotation or not. from now on the question is that what if my variable date is not a published type then what is the way to get the Published\<Timer> type object? To answer this question you should know how a property wrapper works.

🔥 Correction: Projected Value_updated 2023.03.15

Given @State var flag = true, It has three different ways to retrieve its value by purpose:

flag vs $flag vs_flag

  1. flag: Bool (wrappedValue)
  2. $flag: Bnding (projectedValue)
  3. _flag: State

While I said $ preposition property means the storage part of property Wrapper, I should correct it. $property means literally projectedValue not wrappedValue. Here's a thing. this below code block is from SwiftUI's State and you can see how it defines wrappedValue and projectedValue.

@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 }
}

Keep this above code in your mind and see this next example.

struct LoginView: View {
  @State var email = ""
  @State var password = ""
  
  var body: some View {
  	VStack {
  		TextField("Email", text: $email)
  ...
  

Have you ever faced the situation that get the value of @State to child view as @Binding value? So familar. Right? In this situation, you should refer state's value in this way: $email. If you don't, our compiler gives error like this.

If you still remember how the struct State is defined in your mind, you would get what is going on. just use var email means that just fetching its String value, wrappedValue. But if you put the preposition on var email, the compiler get the projectedValue which conforms Binding type.

Property Wrapper 💅

https://github.com/apple/swift-evolution/blob/61e0230f060a86fdf9be22a4288dd08c2a810a68/proposals/0258-property-wrappers.md

When and Why

  • It has a repeated implementation that computes the property's value when it's being got or set
  • such as giving default value by type when it's nil
  • get/set in userDefaults
  • or even implementing lazy, @NSCopying
struct GlobalSettings {
    private let fooKey = "FOO_FEATURE_ENABLED"
    private let barKey = "BAR_FEARTUE_ENABLED"
    
    var foo: Bool {
        get {
            return UserDefaults.standard.bool(forKey: fooKey)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: fooKey)
        }
    }
    
    var bar: Bool {
        get {
            return UserDefaults.standard.bool(forKey: barKey)
        }
        set{
            UserDefaults.standard.set(newValue, forKey: barKey)
        }
    }
}
    

It has a repeated logic that set or gets userDefaults every time the property is got or set. Therefore it creates a lot of boiler codes. However, by adopting the property wrapper, we can let API know how to process the value. Once you define a property wrapper class and afterward write an annotation at the upper side of a property that you want to apply a computation, API can convert an original value to a new value following our rule.

Let's say we want to make a lazy property. The term lazy means to make a variable initialized later, which is differ from an ordinary property initialized immediately. And the lazy variable can't be modified once it is initialized. We don't have to implement this feature from the ground, thanks to Swift offering the lazy property already as a primitive feature. But The following code is one of the ways to implement lazy logic assuming we don't have support from Swift.

The old way

class Foo {
  let immediatelyInitialized = "foo"
  var _initializedLater: String?

  // We want initializedLater to present like a non-optional 'let' to user code;
  // it can only be assigned once, and can't be accessed before being assigned.
  var initializedLater: String {
    get { return _initializedLater! }
    set {
      assert(_initializedLater == nil)
      _initializedLater = newValue
    }
  }
}

We use two variables to implement a lazy feature. One is a _initializedLater that is a real storage having the value we are looking for. The other is a computed property named initializedLater that gets or sets_initializedLater, which works like a representative for _initializedLater. The point is we are not directly getting or setting value. we handle value through a representative variable and the representative variable can not be nil. Also it acts like a let variable so that it forced casting original value and guarantees initialised only once.

And this pattern would be repeated every time we make a lazy property. So now on we are gonna create a property wrapper class for implementing laziness and replace the old way.

How?

Confirms to property Wrapper protocol

@propertyWrapper
enum Lazy<T> {
    // associated value는 type을 명시
    case uninitialized(() -> T)
    case initialized(T)
    
    init(wrappedValue initialValue: @autoclosure @escaping () -> T) {
        self = .uninitialized(initialValue)
    }
    
    var wrappedValue: T {
        mutating get {
            switch self {
            case .uninitialized(let initializer):
                let value = initializer()
                self = .initialized(value)
                return value
            case .initialized(let value):
                return value
            }
        }
        set {
            self = .initialized(newValue)
        }
    }
}
  • a struct, a class, an enum etc all is fine
  • T is a generic form standing for a type
  • an init method is optional. But once you would implement it, you should deal with the first parameter named wrappedValue.
  • var wrappedValue is must-implemented and it is the computed property having get/set logic

Use an annotation with my custom property wrapper

@Lazy var foo = 1738

This is simple but little bit puzzling, right? and the above is translated to this.

private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
  get { return _foo.wrappedValue }
  set { _foo.wrappedValue = newValue }
}

It looks familiar right? this have two variable. One is the storage of Lazy type and the other is the representative of primative type-Int.

API reads the annotation and understands the below variable as the combination of the storage following our custom rule and the representative retrieving the storage value.

The preposition 💲

@CopyOnWrite var x = UIBezierPath()
print($x)                 // okay to refer to compiler-defined $x
let $y = UIBezierPath()   // error: cannot declare entity with $-prefixed name '$y'

The official document said identifiers that start with $ is for the projection property. Thus so far, we discussed about two variables, right? the storage and the representative. And the projection property means our the storage.

By this preposition $, The API knows it is time to read the projectedValue holding our value computed by our custom rule (such as Lazy). And this projectedValue is not writable, because it is get-only.

To sum up

 @Published var date: Date = .now
 
 // means the below
 var $date: Published<Date> = Published<Date>(wrappedValue: .now)
 var date: Date {
     get { return $date.value}
     set { $date.value = newValue}
 }
     
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        //publish timer every second
        Timer
            .publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .assign(to: &$date) // assign to = republish to other publisher

Now we can understand why the assign method cry out in need for $date, not date. Yes, $date and date is totally different. $date follows the Publisher and date is just date. It means the API needs $date.

Reference

https://www.dice.com/career-advice/xcode-swiftui-dollar-sign-prefix

profile
느려도 한 걸음 씩 끝까지
post-custom-banner

0개의 댓글