[Swift] Property Wrappers

@State, @Binding, @Trimmed, @CaseInsensitive, etc

Posted by Kim Heebeom on July 7, 2019

Swift Property Wrappers

원문 : Swift Property Wrappers

이전에 Objective-C의 특성에서 언급했던 “at sign”(@)의 존재가, 스위프트가 나오고 조금씩 의문이 풀려가고 있다고 생각했습니다.

처음엔 @ 함수는 Objective-C 영역으로 제한되어 있었습니다(@IBAction, @NSCopying, @UIApplicationMain 등등). 하지만 지금은 스위프트에서 계속 @가 prefix로 들어간 속성들을 통합하고 있습니다.

WWDC 2019에서 SwiftUI 발표를 통해 Swift 5.1을 처음 봤습니다. 그리고 지금까지도 정체를 알 수 없는 속성들이 들어간 감격스런 슬라이드를 보았습니다. : @State, @Binding, @EnvironmentObject

@로 가득 찬 스위프트의 미래를 보았습니다.


About Property Delegates Wrappers

Property wrappers는 SwiftUI의 공식 발표 3개월 전인 19년 3월에 스위프트 포럼에서 처음 언급되었습니다.

Swift Core팀 멤버였던 Douglas Gregor는 lazy 키워드와 같이 언어에서 제공되는 기능에 대한 사용자 일반화(?) 기능이라고 얘기한 바 있다.

게으름은 프로그래밍의 미덕이며, 이렇게 광범위하게 유용한 기능은, 멋진 언어로 만들려는 사려 깊은 설계의 특징입니다. 프로퍼티가 lazy로 선언되면, 프로퍼티에 처음 접근이 될 때 까지 값의 초기화를 지연시킵니다. 동일한 기능을 연산 프로퍼티를 써서 구현할 수도 있지만 lazy 키워드 하나가 이를 모두 불필요하게 만듭니다.

struct Structure {
    // Deferred property initialization with lazy keyword
    lazy var deferred = ...

    // Equivalent behavior without lazy keyword
    private var _deferred: Type?
    var deferred: Type {
        get {
            if let value = _deferred { return value }
            let initialValue = ...
            _deferred = initialValue
            return initialValue
        }

        set {
            _deferred = newValue
        }
    }
}

SE-0258 : Property Wrappers는 현재 네 번째 리뷰에 있으며, lazy와 같은 기능을 개발자가 직접 구현할 수 있도록 할 것입니다.

이 제안은 설계 및 구현을 간략히 보여주는 훌륭한 작업입니다. 따라서 이 설명을 개선하기보다는 Property wrappers가 만들 수 있는 몇 가지 새로운 패턴을 살펴보는 것이 흥미로우리라 생각했습니다. 이 과정에서 프로젝트에서 이 기능들을 더 잘 다룰 수 있게 될 겁니다.

새로운 @propertyWrapper 속성에 대한 네 가지 사용 사례가 있습니다.

  • Constraining Values: 제약 값
  • Transfoming Values on Property Assignment: 속성 할당과 변환
  • Changing Synthesized Equality and Comparison Semantics: 합성 동등 및 비교 의미 변경하기
  • Auditing Property Access: 속성 접근 감시

Constraining Values

SE-0258은 @Lazy, @Atomic, @ThreadSpecific, @Box을 포함한 실용적인 예제들을 많이 제공합니다. 그중에서도 가장 흥미로웠던 것은 @Constrained property wrapper입니다.

스위프트의 표준 라이브러리는 정확하고, 성능이 좋으면서 부동 소수점 숫자나 우리가 원하는 어떤 크기도 가질 수 있습니다.

유효한 범위의 값을 가지는 사용자 정의 부동 소수점 숫자는 스위프트 3에서부터 구현할 수 있었습니다. 하지만 이렇게 하려면 미로와 같은 프로토콜 요구사항을 따라야 했습니다.

이 기능을 끄는 것은 쉬운 일이 아닐뿐더러 대부분의 유스 케이스를 정당화하기에는 굉장히 많은 작업량입니다.

다행히도 Property wrappers는 훨씬 적은 노력으로 표준 숫자 형식을 매개 변수화하는 방법을 제공합니다.

Implementing a value clamping property wrapper

다음 Clamping 구조를 살펴보겠습니다. 이 property wrapper(@propertyWrapper속성으로 표시된)는 지정된 범위를 벗어난 값을 자동으로 “clamps” 합니다.

@propertyWrapper
struct Clamping<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    init(initialValue value: Value, _ range: ClosedRange<Value>) {
        precondition(range.contains(value))
        self.value = value
        self.range = range
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }
}

예를 들어 산성도(0-14)를 나타내는 pH값에 @Clamping을 사용해서 값을 보장할 수 있습니다.

struct Solution {
    @Clamping(0...14) var pH: Double = 7.0
}

let carbonicAcid = Solution(pH: 4.68) // at 1 mM under standard conditions

이 범위를 벗어나는 pH 값을 설정하려고 하면 가장 가까운 경곗값 (최솟값 또는 최댓값)이 대신 사용됩니다.

let superDuperAcid = Solution(pH: -1)
superDuperAcid.pH // 0

❗️❗️❗️
프로퍼티 래퍼의 구현에서도 다른 프로퍼티 래퍼를 사용할 수 있습니다. 이 UnitInterval 프로퍼티 래퍼는 0과 1 사이의 값으로 제한하기 위해 @Clamping을 사용했습니다.

@propertyWrapper
struct UnitInterval<Value: FloatingPoint> {
    @Clamping(0...1)
    var wrappedValue: Value = .zero

    init(initialValue value: Value) {
        self.wrappedValue = value
    }
}

예를 들어 @UnitInterval 프로퍼티 래퍼를 사용해서 빨강, 초록, 파랑의 정도를 백분율로 나타내는 RGB 타입을 정의할 수도 있습니다.

struct RGB {
    @UnitInterval var red: Double
    @UnitInterval var green: Double
    @UnitInterval var blue: Double
}

let cornflowerBlue = RGB(red: 0.392, green: 0.584, blue: 0.929)
  • 양수, 음수가 아닌 정수임을 보장하는 @Positive / @NonNegative
  • 숫자 값이 0이 아님을 보장하는 @NonZero
  • 할당할 수 있는 값을 제한하는 @Validated 또는 @Whitelisted / @Blacklistedproperty

Transforming Values on Property Assignment

사용자로부터 텍스트 입력을 받는 것은 앱 개발자들 사이에는 영원한 골칫거리입니다. 문자열 인코딩의 아주 진부한 방식에서 텍스트 필드를 통해 코드를 주입하려는 악의적인 시도부터 많은 종류가 있습니다. 그중에서도 개발자가 직면하는 가장 미묘하고 좌절스러운 문제는 콘텐츠의 앞뒤 공백을 다루는 것입니다.

문자열 앞에 단일 공백 하나로 인해 URL이 제대로 생성되지 않고, Date 파서에서도 문제를 일으킵니다.

import Foundation

URL(string: " https://nshipster.com") // nil (!)

ISO8601DateFormatter().date(from: " 2019-06-24") // nil (!)

let words = " Hello, world!".components(separatedBy: .whitespaces)
words.count // 3 (!)

사용자 입력에 관해서는 클라이언트가 가장 자주 무지(알 필요가 없다)를 주장하고 모든 것을 그대로 서버로 보냅니다. ¯\_(ツ)_/¯

클라이언트가 더 많은 책임을 갖도록 옹호하는 것은 아니지만 상황에 따라 Swift의 프로퍼티 래퍼에 대한 또 하나의 매력적인 사례가 됩니다.

Foundation은 trimmingCharacters(in:) 메서드를 Swift strings에 연결합니다. 이 메서드는 공백을 없애주는 메소드 중에서 가장 편리한 방법을 제공합니다. 하지만 이 메서드를 호출할 때 데이터가 정상적이라는 걸 보장하기가 쉽지 않습니다. 이 말에 어느 정도 동의한다면 아마 더 나은 접근법이 있는지 궁금할 것 입니다.

조금 덜 임시적인 접근법으로, willSet 속성 콜백을 사용할 수 있지만, 이미 사용 중인 값을 변경하는 데에는 사용할 수 없다는 것을 알고 실망하게 될 것입니다.

struct Post {
    var title: String {
        willSet {
            title = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
            /* ⚠️ Attempting to store to property 'title' within its own willSet,
                   which is about to be overwritten by the new value              */
        }
    }
}

아마 거기에서 didSet이란 녀석에 대한 잠재력을 깨달았을 수도 있습니다… 단지 나중에 didSet은 초기화 시점엔 호출되지 않는다는 것을 알게 되겠지만..

struct Post {
    var title: String {
        // 😓 Not called during initialization
        didSet {
            self.title = title.trimmingCharacters(in: .whitespacesAndNewlines)
        }
    }
}

❗️ didSet안에서 프로퍼티를 설정해도 콜백이 다시 호출되지 않으니 무한루프를 염려할 필요는 없습니다.

다른 방법들을 시도해볼 수 있긴 하지만 여러 특성을 모두 만족시키는 방법은 없습니다. 이런 경험이 있었다면, 이젠 더는 해결책을 찾아다닐 필요가 없어졌습니다. property wrappers가 우리가 찾던 해결책이니까요.!

Implementing a Property Wrapper that Trims Whitespace from String Values

자, 들어오는 문자열에서 공백과 개행문자를 잘라내는 Trimmed struct를 만들어봅시다.

import Foundation

@propertyWrapper
struct Trimmed {
    private(set) var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }

    init(initialValue: String) {
        self.wrappedValue = initialValue
    }
}

Post 구조체에서 각 문자열 프로퍼티에 @Trimmed 어노테이션을 표시하면 초기화 또는 이후 값의 변화가 있을 때마다 자동으로 공백문자를 제거합니다.

struct Post {
    @Trimmed var title: String
    @Trimmed var body: String
}

let quine = Post(title: "  Swift Property Wrappers  ", body: "...")
quine.title // "Swift Property Wrappers" (no leading or trailing spaces!)

quine.title = "      @propertyWrapper     "
quine.title // "@propertyWrapper" (still no leading or trailing spaces!)
  • 문자열에 ICU transforms을 적용하는 @Transformed
  • 사용자 정의 normalization form을 제공하는 @Normalized
  • “가장 가까운 ½지점까지 반올림” 처럼 정밀한 중간값을 추적하는 @Quantized / @Rounded / @Truncated

Changing Synthesized Equality and Comparison Semantics

⚠️ 이 동작은 합성된 프로토콜 구현 세부사항에 따라 달라지며 이 기능이 완료되기 전까진 변경될 수 있습니다.

스위프트에서 두 문자열 값은 canonically equivalent인 경우 동일한 것으로 간주합니다. 그래서 대부분의 경우에 스위프트 문자열은 예상한 대로 잘 동작합니다. 두 문자열이 동일한 문자로 구성되어 있다면 사전 합성(미리 합쳐진) 문자열인지 아니면 개별 문자로 구성된 건지 중요하지 않습니다. - 즉,
“é”(U+00E9 LATIN SMALL LETER E with ACUTE)는
“e”(U+0065 LATIN ETERET) + “◌́” (U+0301 COMBINING ACUTE ACCENT)와 동일합니다.

하지만 만약에 대소문자를 구별하지 않는 문자열 비교처럼 다른 equality semantics를 원한다면 어떨까요.

현재 기존 언어의 기능을 사용하여 구현할 수 있는 방법은 여러 가지가 있습니다.

  • 항상 lowercased()의 결과를 이용해서 비교하는 방법이 있습니다.. 하지만 이런 수동 작업은 버그를 만들 가능성이 큽니다.
  • CaseInsensitive처럼 새로운 타입을 정의하고 문자열값을 래핑하는 방법입니다. 하지만 String타입에 대한 기능적인 추가 작업이 많이 들어갑니다.
  • 기존 비교함수를 래핑에서 custom comparator를 만드는 방법이 있습니다. but nothing comes close to an unqualified == between two operands.

이 옵션 중에 특별히 매력적인 것이 없기도 하고, 고맙게도 property wrappers를 쓰면 이 문제를 해결할 수 있습니다.


❗️❗️❗️
스위프트는 문자열도 숫자와 마찬가지로 작게 정의된 프로토콜 집합에 책임을 위임하는 프로토콜지향 접근 방식을 사용합니다.

각자 자신만의 String-equivalent 타입을 만들 수 있지만 문서에는 다음과 같이 강력한 지침이 있습니다.

Do not declare new conformances to StringProtocol. Only the String and Substring types in the standard library are valid conforming types.

Implementing a case-insensitive property wrapper

아래의 CaseInsensitive타입은 String/SubString 값을 중심으로 프로퍼티 래퍼를 구현합니다. 이 타입은 브릿지된 NSString API의 caseInsensitiveCompare(_:)를 통해 Comparable를 따릅니다.

import Foundation

@propertyWrapper
struct CaseInsensitive<Value: StringProtocol> {
    var wrappedValue: Value
}

extension CaseInsensitive: Comparable {
    private func compare(_ other: CaseInsensitive) -> ComparisonResult {
        return wrappedValue.caseInsensitiveCompare(other.wrappedValue)
    }

    static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
        return lhs.compare(rhs) == .orderedSame
    }

    static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
        return lhs.compare(rhs) == .orderedAscending
    }

    static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
        return lhs.compare(rhs) == .orderedDescending
    }
}

❗️> 연산자는 자동으로 생성되지만, 여기서는 성능 최적화를 하고자 caseInsensitiveCompare 기본 메서드에 대한 불필요한 호출을 막았습니다.

대소문자만 다른 두 문자열 값을 만들어서 표준 동등비교를 사용하면 결과로 false가 나오지만, 문자열을 래핑한 CaseInsensitive객체를 사용하면 true가 나옵니다.

let hello: String = "hello"
let HELLO: String = "HELLO"

hello == HELLO // false
CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true

지금까지 이 접근 방식은 위에서 설명한 커스텀 래퍼와 다를바가 없습니다. 이것은 ExpressibleByStringLiteral과 다른 모든 프로토콜을 준수해서 CaseInsensitiveString인것처럼 느껴지도록 하기 위한 오랜 노력에서 나왔습니다.

프로퍼티 래퍼는 이러한 모든 불필요한 작업들을 하지않도록 해줍니다.

struct Account: Equatable {
    @CaseInsensitive var name: String

    init(name: String) {
        $name = CaseInsensitive(wrappedValue: name)
    }
}

var johnny = Account(name: "johnny")
let JOHNNY = Account(name: "JOHNNY")
let Jane = Account(name: "Jane")

johnny == JOHNNY // true
johnny == Jane // false

johnny.name == JOHNNY.name // false

johnny.name = "Johnny"
johnny.name // "Johnny"

Account객체는 name프로퍼티를 대소문자를 구분하지 않는 비교를 통해 동일성 검사를 합니다. 게다가 name프로퍼티를 가져오거나 설정하려고 하면 진정한 의미의 String값이 됩니다.

깔끔하네요, 근데 실제로 여기서 무슨 일이 벌어지고 있는 걸까요?

스위프트 4 이후, 컴파일러는 특정 타입의 모든 저장 프로퍼티가 Equatable를 따르고, 해당 타입 또한 Equatable를 선언하면 그 타입에 대해선 자동으로 Equatable를 준수하도록 해줍니다(합성한다).

컴파일러 합성이 구현되는 방법 때문에 프로퍼티 래퍼는 기본값이 아니라 래퍼를 통해 판단하게 됩니다.

// Synthesized by Swift Compiler
extension Account: Equatable {
    static func == (lhs: Account, rhs: Account) -> Bool {
        lhs.$name == rhs.$name
    }
}
  • "①""1" 처럼 래핑된 문자열을 동일한 것으로 간주하는 @CompatibilityEquivalence
  • 부동소수점 타입에 대한 equality semantic을 정제하는 @Approximate (SE-0259 참조)
  • 카드놀이의 .ace를 상황에 따라 가장 높거나 가장 낮은 카드로 취급하는 것처럼 엄격한 순서를 정의하는 기능을 하는 @Ranked

Auditing Property Access

누가 언제 어떤 기록에 접근할 수 있는지와 같은 특정 동작을 규정하거나 시간에 따른 form의 변경 사항을 규정하는 것이 비즈니스 요구사항일 수 있습니다.

다시 말하지만, 일반적으로 앱에서 수행하는 동작은 아닙니다. 대부분의 비즈니스 로직은 서버에 정의되어있고, 대부분 그게 맞다고 생각합니다. 하지만 이 경우는 우리가 프로퍼티 래퍼라는 안경을 끼고 봤을 때, 무시할 수 없을 정도로 매력 있는 유스케이스가 될 수 있습니다.

Implementing a Property Value Versioning

Versioned구조체는 값이 설정될 때 마다 타임스탬프를 기록하는 프로퍼티 래퍼이다.

import Foundation

@propertyWrapper
struct Versioned<Value> {
    private var value: Value
    private(set) var timestampedValues: [(Date, Value)] = []

    var wrappedValue: Value {
        get { value }

        set {
            defer { timestampedValues.append((Date(), value)) }
            value = newValue
        }
    }

    init(initialValue value: Value) {
        self.wrappedValue = value
    }
}

가상의 ExpenseReport 클래스는 문서 처리에 대한 상태 변경을 추적하기 위해 @Versioned으로 선언된 state를 감싸고 있습니다.

class ExpenseReport {
    enum State { case submitted, received, approved, denied }

    @Versioned var state: State = .submitted
}
  • 값을 읽거나 쓸 때마다 시간을 기록하는 @Audited
  • 값을 읽을 때마다 설정된 숫자값을 나누는 @Decaying

이 특정 예에서는 스위프트의 오랜 결핍으로 인해 발생하는,, 프로퍼티 래퍼 구현에 대한 중요한 제한 사항을 보여줍니다. 프로퍼티를 throws로 선언할 수 없습니다.

에러 처리를 할 수 없는 프로퍼티 래퍼는 정책을 따르거나 전달할 수 있는 합리적인 방법을 제공하지 않습니다. 예를 들어 @Versioned 프로퍼티 래퍼를 확장해서, 이전에 .denied였던 적이 있다면, 후에 .approved상태가 될 수 없도록 하고 싶다면, 가장 좋은 방법은 fatalError()를 쓰는 방법밖에 없다. 하지만 실제 앱에서 이것은 적합하지 않다.

class ExpenseReport {
    @Versioned var state: State = .submitted {
        willSet {
            if newValue == .approved,
                $state.timestampedValues.map { $0.1 }.contains(.denied)
            {
                fatalError("J'Accuse!")
            }
        }
    }
}

var tripExpenses = ExpenseReport()
tripExpenses.state = .denied
tripExpenses.state = .approved // Fatal error: "J'Accuse!"

이 경우는 우리가 지금까지 프로퍼티 래퍼를 사용하면서 부딪혀왔던 몇 가지 제한사항 중 하나입니다. 프로퍼티 래퍼에 대한 균형 잡힌 시각을 위해서 나머지 부분은 제한사항에 대해 살펴보겠습니다.

Limitations

⚠️ 아래에 설명한 단점 중에 일부는 언어의 특징 그 자체보단 현재 나의 이해도나 상상력의 한계 때문일 수 있습니다.

Properties Can’t Participate in Error Handling

프로퍼티는 함수와 달리 throws를 표기할 수 없습니다.

이건 두 타입 멤버 사이의 몇 안되는 차이점 중 하나입니다. 속성에는 getter와 setter가 모두 있으므로 오류처리를 추가할 경우, 특히 액세스 제어, 사용자 지정 getter/setter, 콜백 등과 같은 다른 문제에 대한 구문을 사용하는 방법을 고려할 때 올바른 설계가 무엇인지 완전히 알 수 없습니다.

앞에서 얘기했던 것처럼 프로퍼티 래퍼에는 유효하지 않은 값을 처리하기 위한 두 가지 방법이 있습니다.

  • 조용히 무시해버리거나..
  • fatalError()로 표기하기

이 두 방법 모두 딱히 좋진 않다.

Wrapped Properties Can’t Be Aliased

또 다른 제한은 프로퍼티 래퍼의 인스턴스를 프로퍼티 래퍼로 사용할 수 없다는 것입니다.

이전에 보았던 0과 1사이로 값을 제약하는 UnitInterval 예제는 아래처럼 간결하게 표현될 수도 있습니다.

typealias UnitInterval = Clamping(0...1) // ❌

하지만 이건 불가능합니다. 프로퍼티 래퍼의 인스턴스를 사용해서 프로퍼티를 래핑할 수도 없습니다.

let UnitInterval = Clamping(0...1)
struct Solution { @UnitInterval var pH: Double } // ❌

이건 실제로 우리가 생각하는 것보다 더 많은 코드 복제를 하게 될 수도 있습니다. 하지만 언어의 유형과 가치 사이의 근본적인 차이에서 발생한다는 것을 감안하면, 잘못된 추상화를 피하기 위해 어느 정도는 중복을 감수할 수도 있을 듯합니다.

Property Wrappers Are Difficult To Compose

프로퍼티 래퍼의 조합은 상호적인 동작이 아닙니다. 선언하는 순서에 따라 동작의 차이를 보일 수 있습니다.

string inflection 프로퍼티 래퍼와 string transforms프로퍼티 래퍼를 같이 사용하는 경우를 생각해봅시다. 예를 들어 블로그 게시물의 URL “slug”를 자동으로 표준화하기 위한 프로퍼티 래퍼를 구성할 때, @Trimmed 를 해주는 위치에 따라 결과가 달라질 수 있습니다.

struct Post {
    ...
    @Dasherized @Trimmed var slug: String
}

하지만 애초에 이런 동작을 하는 것이 말처럼 쉽진 않습니다. 바깥쪽 래퍼가 안쪽 래퍼 유형의 값에 따라 동작하므로 똑같이 문자열 값에 동작하는 두 개의 프로퍼티 래퍼를 구성하려고 하면 작업이 실패합니다.

@propertyWrapper
struct Dasherized {
    private(set) var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.replacingOccurrences(of: " ", with: "-") }
    }

    init(initialValue: String) {
        self.wrappedValue = initialValue
    }
}

struct Post {
    ...
    @Dasherized @Trimmed var slug: String // ⚠️ An internal error occurred.
}

물론 동작하도록 할 수는 있지만, 명확하게 보이진 않습니다. 구현상에서 수정될 수 있는 건지, 문서화만으로 해결될 수 있는지는 조금 더 지켜봐야 할 듯합니다.

Property Wrappers Aren’t First-Class Dependent Types

종속 타입은 값에 따라 타입이 결정되는 것입니다. 예를 들어, “후자가 전자에 비해 큰 정수 쌍”이나 “소수를 가진 배열”은 둘 다 값에 따라 타입 정의가 달라지므로 종속 타입입니다.

스위프트가 종속 유형에 대한 지원이 부족하다는 것은 이러한 보증이 런타임에 시행되어야 함을 의미합니다.

예를 들어, 프로퍼티 래퍼를 사용해서 값이 가능한 제약조건이 있는 새로운 타입을 정의할 수 없습니다.

typealias pH = @Clamping(0...14) Double // ❌
func acidity(of: Chemical) -> pH {}

또한 프로퍼티 래퍼를 사용해서 컬렉션의 키 또는 값에 주석을 추가할 수도 없습니다.

enum HTTP {
    struct Request {
        var headers: [@CaseInsensitive String: String] // ❌
    }
}

Property Wrappers Are Difficult to Document

Pop Quiz : SwiftUI 프레임 워크에서는 어떤 프로퍼티 래퍼를 사용할 수 있을까용?

정답은 이곳에..

😬

이건 어떻게보면 프로퍼티 래퍼만의 문제는 아니죠.

Property Wrappers Further Complicate Swift

스위프트는 오브젝티브씨 보다 훨씬 더 복잡한 언어입니다. 스위프트 1.0 이후론 시간이 지남에 따라 점점 더 커졌습니다.

스위프트에서 @ prefix의 폭증(스위프트4: @dynamicMemberLookup, @dynamicCallable, Swift for Tensorflow: @differentiable, @memberwise 등등)으로 인해서 이제는 스위프트 API 문서만으로는 합리적인 이해가 점점 더 어려워지고 있습니다.

어떻게 이걸 다 이해할 수 있을까요 😹

Nataliya Patsovska가 트윗에 한 말입니다.

  • iOS API design, short history:
    • Objective C - describe all semantics in the name, the types don’t mean much
    • Swift 1 to 5 - name focuses on clarity and basic structs, enums, classes and protocols hold semantics
    • Swift 5.1 - @wrapped $path @yolo