Swift 속성에 대한 래퍼에 접근하는 방법
게시 됨: 2022-03-11간단히 말해서 속성 래퍼는 속성에 대한 읽기 및 쓰기 액세스를 캡슐화하고 추가 동작을 추가하는 일반 구조입니다. 사용 가능한 속성 값을 제한하거나 읽기/쓰기 액세스에 논리를 추가(예: 데이터베이스 또는 사용자 기본값 사용)하거나 몇 가지 추가 방법을 추가해야 하는 경우 사용합니다.
이 기사는 새롭고 깔끔한 구문을 소개하는 속성 래핑에 대한 새로운 Swift 5.1 접근 방식에 관한 것입니다.
오래된 접근 방식
응용 프로그램을 개발 중이고 사용자 프로필 데이터가 포함된 개체가 있다고 상상해 보십시오.
struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)
이메일 확인을 추가하고 싶습니다. 사용자 이메일 주소가 유효하지 않은 경우 email
속성은 nil
이어야 합니다. 이것은 속성 래퍼를 사용하여 이 논리를 캡슐화하는 좋은 경우입니다.
struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }
계정 구조에서 이 래퍼를 사용할 수 있습니다.
struct Account { var firstName: String var lastName: String var email: Email<String> }
이제 이메일 속성에는 유효한 이메일 주소만 포함될 수 있습니다.
구문을 제외하고 모든 것이 좋아 보입니다.
let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)
속성 래퍼를 사용하면 이러한 속성을 초기화하고 읽고 쓰기 위한 구문이 더 복잡해집니다. 따라서 이러한 복잡성을 피하고 구문 변경 없이 속성 래퍼를 사용할 수 있습니까? Swift 5.1에서 대답은 예입니다.
새로운 방법: @propertyWrapper 주석
Swift 5.1은 @propertyWrapper
주석으로 속성 래퍼를 표시할 수 있는 속성 래퍼 생성에 대한 보다 우아한 솔루션을 제공합니다. 이러한 래퍼는 기존 래퍼에 비해 구문이 더 간결하여 코드가 더 간결하고 이해하기 쉽습니다. @propertyWrapper
주석에는 단 하나의 요구 사항이 있습니다. 래퍼 객체에는 wrappedValue
라는 비정적 속성이 포함되어야 합니다.
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
코드에서 이러한 래핑된 속성을 정의하려면 새 구문을 사용해야 합니다.
@Email var email: String?
따라서 속성을 @ 주석으로 표시했습니다.
email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil
좋습니다. 이전 접근 방식보다 지금이 더 좋아 보입니다. 그러나 우리의 래퍼 구현에는 한 가지 단점이 있습니다. 래핑된 값에 대한 초기 값을 허용하지 않는다는 것입니다.
@Email var email: String? = "[email protected]" //compilation error.
이 문제를 해결하려면 래퍼에 다음 초기화 프로그램을 추가해야 합니다.
init(wrappedValue value: Value?) { self.value = value }
그리고 그게 다야.
@Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil
래퍼의 최종 코드는 다음과 같습니다.
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
구성 가능한 래퍼
다른 예를 들어보겠습니다. 게임을 작성 중이고 사용자 점수가 저장되는 속성이 있습니다. 요구 사항은 이 값이 0보다 크거나 같고 100보다 작거나 같아야 한다는 것입니다. 속성 래퍼를 사용하여 이를 달성할 수 있습니다.

@propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0
이 코드는 작동하지만 일반적이지 않은 것 같습니다. 다른 제약 조건(0과 100이 아님)으로 재사용할 수 없습니다. 또한 정수 값만 제한할 수 있습니다. Comparable 프로토콜을 준수하는 모든 유형을 제한할 수 있는 하나의 구성 가능한 래퍼를 갖는 것이 좋습니다. 래퍼를 구성 가능하게 만들려면 이니셜라이저를 통해 모든 구성 매개변수를 추가해야 합니다. 이니셜라이저에 wrappedValue
속성(속성의 초기 값)이 포함되어 있으면 첫 번째 매개변수여야 합니다.
@propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }
래핑된 속성을 초기화하기 위해 주석 뒤에 괄호 안에 모든 구성 속성을 정의합니다.
@Constrained(0...100) var scores: Int = 0
구성 속성의 수는 무제한입니다. 이니셜라이저에서와 같은 순서로 괄호 안에 정의해야 합니다.
래퍼 자체에 대한 액세스 권한 얻기
래퍼 자체에 액세스해야 하는 경우(래핑된 값 아님) 속성 이름 앞에 밑줄을 추가해야 합니다. 예를 들어 계정 구조를 살펴보겠습니다.
struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)
추가한 추가 기능을 사용하려면 래퍼 자체에 액세스해야 합니다. 예를 들어, 계정 구조가 Equatable 프로토콜을 따르기를 원합니다. 두 계정은 이메일 주소가 같으면 같으며 이메일 주소는 대소문자를 구분하지 않아야 합니다.
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }
작동하지만 이메일을 비교할 때마다 소문자() 메서드를 추가해야 한다는 점을 기억해야 하기 때문에 최상의 솔루션은 아닙니다. 더 나은 방법은 이메일 구조를 동일하게 만드는 것입니다.
extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }
래핑된 값 대신 래퍼를 비교합니다.
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }
예상 가치
@propertyWrapper
주석은 예상 값인 구문 설탕을 하나 더 제공합니다. 이 속성은 원하는 모든 유형을 가질 수 있습니다. 이 속성에 액세스하려면 속성 이름에 $
접두사를 추가해야 합니다. 작동 방식을 설명하기 위해 Combine 프레임워크의 예를 사용합니다.
@Published
속성 래퍼는 속성에 대한 게시자를 만들고 이를 예상 값으로 반환합니다.
@Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher
보시다시피 메시지를 사용하여 래핑된 속성에 액세스하고 $message를 사용하여 게시자에 액세스합니다. 래퍼에 예상 값을 추가하려면 어떻게 해야 합니까? 특별한 것은 없으며 그냥 선언하십시오.
@propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }
앞서 언급한 것처럼 projectedValue
속성은 필요에 따라 모든 유형을 가질 수 있습니다.
제한 사항
새 속성 래퍼의 구문은 좋아 보이지만 몇 가지 제한 사항도 포함되어 있습니다. 주요 제한 사항은 다음과 같습니다.
- 그들은 오류 처리에 참여할 수 없습니다. 래핑된 값은 속성(메소드 아님)이며 getter 또는 setter를
throws
로 표시할 수 없습니다. 예를 들어Email
예에서 사용자가 잘못된 이메일을 설정하려고 하면 오류를 발생시킬 수 없습니다.nil
을 반환하거나fatalError()
호출로 앱을 충돌시킬 수 있습니다. 이는 어떤 경우에는 허용되지 않을 수 있습니다. - 속성에 여러 래퍼를 적용하는 것은 허용되지 않습니다. 예를 들어,
@Email
래퍼를 대소문자를 구분하지 않는 대신 별도의@Email
래퍼를 가지고@CaseInsensitive
래퍼와 결합하는 것이 좋습니다. 그러나 이러한 구성은 금지되어 있으며 컴파일 오류로 이어집니다.
@CaseInsensitive @Email var email: String?
이 특정 경우에 대한 해결 방법으로 CaseInsensitive
래퍼에서 Email
래퍼를 상속할 수 있습니다. 그러나 상속에도 제한이 있습니다. 클래스만 상속을 지원하고 하나의 기본 클래스만 허용됩니다.
결론
@propertyWrapper
주석은 속성 래퍼의 구문을 단순화하고 일반 속성과 동일한 방식으로 래핑된 속성으로 작업할 수 있습니다. 이것은 Swift 개발자로서 코드를 더 간결하고 이해하기 쉽게 만듭니다. 동시에 고려해야 할 몇 가지 제한 사항이 있습니다. 나는 그들 중 일부가 미래의 Swift 버전에서 수정되기를 바랍니다.
Swift 속성에 대해 자세히 알아보려면 공식 문서를 확인하세요.