2024. 7. 2. 13:51ㆍSwift, iOS Foundation
1️⃣ 왜 Property Wrapper가 필요하게 된 거지?
프로퍼티 래퍼(Property Wrapper)는 클래스나 구조체의 연산 프로퍼티의 기능을 개선하기 위해 Swift 5.1부터 등장한 문법이다.
연산 프로퍼티의 자세한 내용은 아래 글에 정리해뒀으니 읽어보길 바란다.
간략하게 연산 프로퍼티의 개념을 리마인드하자면, get(접근자)과 set(생성자)로 구성되어 있어 저장 프로퍼티를 연산하는 문법이었다.
프로퍼티 래퍼 (Property Wrapper)는 클래스나 구조체에서 사용되는 이 연산 프로퍼티의 반복되는 기능을 "효율적으로 재사용"하게 도와준다.
반복되는 기능이 어떤 의미인지 아래 예시를 통해 이해해 보자.
User라는 구조체에서 name과 email 값을 받을 때 모두 대문자로 바꾸는 연산을 연산 프로퍼티(set)에서 수행하고 있다.
이때, name과 email 연산 프로퍼티는 동일한 연산을 함에도 불구하고, 같은 로직(newValue.uppercased())을 반복해서 작성하고 있는 것을 확인할 수 있다.
struct User {
private var _name: String = ""
private var _email: String = ""
var name: String {
get { _name }
set { _name = newValue.uppercased() }
}
var email: String {
get { _email }
set { _email = newValue.uppercased() }
}
}
var user = User()
user.name = "mini"
user.email = "mini@example.com"
print(user.name) // "MINI"
print(user.email) // "MINI@EXAMPLE.COM"
2️⃣ 프로퍼티 래퍼 (Property Wrapper) 사용법 이해하기
위와 같이 반복되는 연산 프로퍼티의 로직을 매번 작성하는 대신, 프로퍼티 래퍼(Property Wrapper)로 구현할 수 있다.
프로퍼티 래퍼는 @PropertyWrapper 키워드를 붙여서 선언된다.
*Property Wraper라는 이름은 클래스, 구조체, 열거형 내의 프로퍼티를 한번 감싸 같은 로직을 사용할 수 있게끔 만든다는 의미에서 비롯되었다고 볼 수 있다.
Property Wrapper 안에는 반드시 값을 접근하거나 지정하는 (=get, set 코드를 포함하는) wrappedValue 프로퍼티를 가져야 한다.
위의 연산 프로퍼티에서 반복적으로 수행되던 로직이 해당 프로퍼티에 들어간다고 보면 되겠다.
wrappedValue의 초기값을 설정하는 초기화 메서드 init(wrappedValue:)는 필요한 경우에만 사용하면 된다. -> 이 부분은 아래에서 더 자세하게 살펴보는 걸로!
@propertyWrapper
struct FixUpperValue {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.uppercased() }
}
}
해당 프로퍼티 래퍼를 사용하는 경우는 매우 단순하다.
클래스, 구조체, 열거형 내의 프로퍼티 앞에 @키워드와 함께 정의해 둔 PropertyWrapper의 이름을 붙여주기만 하면 된다.
그럼 해당 프로퍼티에 대해 wrappedValue에서 정의해둔 연산이 적용된다. -> name, email 프로퍼티는 wrappedValue setter 메서드에 정의되어 있는 uppercased() 연산이 적용되어 저장된다.
struct User {
@FixUpperValue var name: String
@FixUpperValue var email: String
}
var user = User()
user.name = "mini"
user.email = "mini@example.com"
print(user.name) // "MINI"
print(user.email) // "MINI@EXAMPLE.COM"
3️⃣ 복잡한 프로퍼티 래퍼 (Property Wrapper) 사용하기 : init, Generic 활용
위에서 배운 프로퍼티 래퍼(Property Wrapper)의 개념을 더욱 확장해서 사용해 보겠다.
아래는 초기값(init)으로 min, max 값을 입력받고 wrappedValue setter에서 값이 이 특정 범위 안에 있는지를 검사해서 value에 할당하는 예제이다.
이때 비교 대상의 값과 프로퍼티의 타입을 Generic으로 선언해 정수뿐 아니라, 문자, Date 등등에 활용할 수도 있다.
@propertyWrapper
struct MinMaxVal<V: Comparable> {
var value: V
let max: V
let min: V
var wrappedValue: V {
get { return value }
set {
if (newValue > max) { value = max }
else if (newValue < min) { value = min }
else { value = newValue }
}
}
init(wrappedValue: V, max: V, min: V) {
self.value = wrappedValue
self.max = max
self.min = min
}
}
아래는 위의 프로퍼티 래퍼를 받아서 Int와 Date 타입에 적용을 시킨 모습이다.
// Int로 MinMaxVal Property Wrapper 적용
struct TestInt {
@MinMaxVal(min: 1, max: 10) var number: Int = 1
}
var testInt = TestInt()
testInt.number = 12
print(testInt.number) // 10
// Date로 MinMaxVal Property Wrapper 적용
struct TestDate {
@MinMaxVal(
min: Date(),
max: Calendar.current.date(byAdding: .month, value: 1, to: Date())!
) var date: Date = Date()
}
var testDate = TestDate()
testDate.date = Calendar.current.date(byAdding: .month, value: 2, to: Date())!
print(testDate.date) // "2024-08-02 03:50:42 +0000"
4️⃣ UIkit 코드에서 Property Wrapper 적용하기 (UITextField의 입력값 검증)
Property Wrapper는 UIKit 코드보다는 SwiftUI 코드에서 많이 사용된다. (그렇다고 UIKit에서 아예 사용될 수 없는 것은 아니다.)
그렇지만 나는 이번 글에서 UIKit 기준으로 Property Wrapper를 사용할 수 있는 간단한 예제를 만들어볼까 한다.
텍스트 필드에 4자리 이하의 값을 입력받고 싶은 상황에서, 그 입력값이 4글자 이하인지를 검증하는 로직을 Property Wrapper에 아래와 같이 구현했다.
@propertyWrapper
struct ValidatedInput {
private var value: String = ""
private let validation: (String) -> Bool
var wrappedValue: String {
get { value }
set { value = validation(newValue) ? newValue : "4글자 이하로 입력해주세요" }
}
init(validation: @escaping (String) -> Bool) {
self.validation = validation
}
}
ViewController에는 검증 로직을 $0.count < 5의 형태로 클로저에 담았고,
프로퍼티 래퍼에서 setter에서 판단된 로직에 따라 텍스트필드 아래에 있는 UILabel에 띠울 수 있도록 didSet을 연결했다.
5글자 이상을 입력한 경우, wrappedValue에 따라 myLabel에는 "4글자 이하로 입력해 주세요"가 보이게 되는 상황!
final class ViewController: UIViewController {
// MARK: - Properties
@ValidatedInput(validation: { $0.count < 5 })
var validatedInput: String {
didSet {
myLabel.text = validatedInput
}
}
그리고 텍스트 필드에 addTarget 메서드를 걸어서. editingChanged 될 때마다 값을 해당 프로퍼티에 반영하도록 설정해 두면,
아래 화면과 같이 SwiftUI처럼 실시간 정보가 바로 Label에 반영되게 된다.
func setupTarget() {
myTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
@objc
func textFieldDidChange(_ textField: UITextField) {
self.validatedInput = myTextField.text ?? ""
}
실제 이렇게 사용하는 건지는 모르겠.. 지만
아무튼 위에 PropetyWrapper나 연산 프로퍼티의 특성이 입력값을 검증하는 과정에서 많이 사용될 수 있기에, 이런 식으로 활용할 수 있지 않을까 해서 만들어보게 되었다.
나중에 실전 코드에서 적용되는 날이 있으면 다시 돌아오기로 기약하며,
오늘 내용은 여기까지!
'Swift, iOS Foundation' 카테고리의 다른 글
[Swift] ARC (Automatic Reference Counting) 완전 정복하기 (0) | 2024.07.06 |
---|---|
[Swift] 제네릭 (Generic) 완전 정복하기 (0) | 2024.07.03 |
[Swift] @discardableResult란 무엇일까? (10) | 2024.04.18 |
[Swift] for-in문과 forEach 사이의 차이, 그리고 고차함수(map, filter, reduce)의 활용까지 (0) | 2024.03.11 |
[Swift] Swift는 에러를 어떻게 처리할까? (Error Handling) (1) | 2024.02.13 |