최근 WWDC를 2019년도 버전부터 찾아보는 습관을 들이고있는 과정에서
SwiftUI
와 함수형 프로그래밍
에 대해 관심을 갖게되었네요!
SwiftUI를 학습하기 위해서는 가장 기본적으로 맞이하게되는 @PropertyWrapper
에 대해
알고 넘어가야할 필요성을 느껴 학습하며 정리한 내용입니다.
PropertyWrapper
는 값 저장을 관리하는 코드를 재사용하기 위한 매커니즘을 제공하기 위해서
Swift 5.1
에서 공개되었습니다.
PropertyWrapper를 사용하면 특정 동작 또는 로직을 캡슐화(Encapsulation)❗️
하여 여러 프로퍼티에 쉽게 재사용
할 수 있다는 장점이 있습니다.
기본적인 코드를 살펴보며 매커니즘을 이해해보죠!
// 12 이하로 제한되도록 하는 프로퍼티를 생성하는 propertyWrapper 정의
import Foundation
@propertyWrapper
struct TwelveOrLess {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
struct SmallRectangle {
@TwelveOrLess var width: Int
@TwelveOrLess var height: Int
}
/* 명시적으로 사용하는 경우
struct SmallRectangle {
private var _width = TwelveOrLess()
private var _height = TwelveOrLess()
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
}
*/
var smallRect = SmallRectangle()
print(smallRect.width, smallRect.height) // 0, 0
smallRect.width = 10
smallRect.height = 15
print(smallRect.width, smallRect.height) // 10, 12
PropertyWrapper를 사용하기 위해서는 @propertyWrapper
라는 키워드를 사용하여 프로퍼티가
저장되는 방식을 관리하는 코드
와 프로퍼티를 정의하는 코드
사이에 분리 계층을
추가합니다. (즉, 값의 저장과 접근을 관리하는 로직 2가지로 존재)
SmallRectangle
에서 width, height
에서 볼 수 있듯이 PropertyWrapper를 적용하려면
프로퍼티 선언 앞에 @
와 wrapper의 이름
을 붙입니다.
이렇게 하면 해당 프로퍼티의 저장과 접근 방식이 Wrapper에 정의된 대로 처리되죠!
(결과값이 10, 12가 나온 이유를 알 수 있습니다.)
Apple의 공식문서에서는 아래와 같은 케이스에서 적용하면 좋다고 합니다.
- Thread의 안전 검사를 제공하기 위한 경우
- 데이터베이스에 저장하는 프로퍼티가 있는 경우 (Entity??)
WWDC19에서 사용한 UserDefaults 예제가 있으며,
개발하는 정대리님의 강의에서는 API를 받아올 때 Int
타입으로 명시는 되어있지만
String
타입으로 받아오는 경우에 사용하는 방법을 보여주셔서 다양한 경우에 적용되는 것 같습니다.
//MARK: - UserDefault 만들기
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? self.defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: self.key)
}
}
}
class UserManager {
@UserDefault(key: "userID", defaultValue: false)
static var userID: Bool
@UserDefault(key: "myEmail", defaultValue: nil)
static var myEmail: String?
@UserDefault(key: "isLoggedIn", defaultValue: false)
static var isLoggedIn: Bool
}
//MARK: - TodoResponse
struct TodoResponse: Codable {
let createAt, title: String
let id: String
@AceeptStringToInt
var viewCount: Int
}
@propertyWrapper
struct AceeptStringToInt: Codable {
let wrappedValue: Int
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let stringValue = try? container.decode(String.self) {
self.wrappedValue = Int(stringValue) ?? 0
} else {
self.wrappedValue = try container.decode(Int.self)
}
}
}
Swift에서는 다양한 프로퍼티 구현 패턴을 간편하게 만들기 위해
이전에는 특정한 언어 차원에서의 기능을 지원하였습니다.
하여 공식 문서에서 이야기하는 차원이 lazy
키워드와 NSCopying
이며
lazy
는 객체가 실제로 필요할 때까지 초기화를 지연시키고 이는 Swift 언어 초창기 시절부터 내장되어 사용되어왔죠.
NSCopying
은 Objective-C에서 가져온 것으로, 프로터티의 값이 복사되어 할당되도록 클래스에서 깊은 복사를 사용할 때 사용하는 방식 중 하나였죠.
이러한 특정 패턴들은 언어 차원에서 직접 지원하는 것으로 한계가 있었다고 합니다.
- 제한된 범위와 유용성
- 유연성 부족
lazy와 NSCopying 같은 특성은 특정 사용 사례에만 유용하였고...
다른 종류의 패턴을 구현하려면 개발자가 많은 양의 보일러플레이터(반복적인) 코드를 작성해야만 했습니다.
즉, 재사용성에서 부족한 점이 보였다는 점이였죠..
새로운 프로퍼티 패턴이 필요할 때마다 언어 자체를 확장하거나 변경해야하는 문제가 있었고 이는 개발자들이 자신만의 커스텀 프로퍼티 패턴을 작성하는데 어려움이 발생했다는 점입니다.
즉, PropertyWrapper
는 lazy와 NSCopying의 언어 차원에서 지원되는 특정 패턴에 국한되지 않고 더 넓은 범위의 패턴에서 유연하게 지원하기 위해 도입된 매커니즘으로서 편리함을 제공하기 위함이였네요❗️
위 내용을 바탕으로 Example로 구현되어있는 내용들이 있어 함께 살펴보면 좋을 것 같습니다 👍🏻
(WWDC19| Modern Swift API Design 에서도 NSCopying과
Delayed Initialization에 대해 보여준 내용이 꽤 어려웠지만 흥미로웠습니다!)
PropertyWrapper 내에서 projectedValue
라는 이름의 프로퍼티를 정의함으로써,
개발자는 특정 프로퍼티에 대한 추가적인 정보나 기능을 제공할 수 있습니다.
아래의 예시 코드를 보면 훨씬 이해하기가 쉬웠습니다.
@propertyWrapper
struct ExampleWrapper {
var wrappedValue: Int
var projectedValue: String {
return "Current Value: \(wrappedValue)"
}
}
struct ExampleStruct {
@ExampleWrapper var number: Int
}
var example = ExampleStruct(number: 5)
print(example.$number) // "Current Value: 5" 출력
projectedValue
란 $
키워드로 해당 값에 접근할 수 있는 기능으로
wrappedValue
로 wrapping된 값에 직접 접근하는 방법이 아닌
부가적으로 프로퍼티에 접근하는 방법입니다.
아직 Combine에 대해 학습하지 못하였지만 해당 내용을 많이 접할 수 있다고 하네요..😋
Properties | Documentation - Swift.org
Github | swift-evolution/proposals/propertyWrapper
Modern Swift API Design - WWDC19 - Videos
Property Wrapper - ZeddiOS - 티스토리
취준생을 위한 iOS 앱만들기 - API 파싱 propertyWrapper - iOS Dev Tutorial (2022)
[iOS - swift] @propertyWrapper의 projectedValue 개념 ($ 접두사, 달러 접두사)