Swift Properties Wrapper คือ

Cocodev
Lotus’s IT
Published in
2 min readDec 18, 2023

เชื่อว่า iOS developer เคยที่เขียน SwiftUI ต้องผ่านการใช้งาน @State, @Binding, และ @ObservedObject มาแล้วอย่างแน่นอน ซึ่ง Syntax ที่มี @{Name} แบบนี้เรียกว่า "Properties Wrapper" ในบทความนี้ เราจะช่วยคุณ ทำความรู้กับมัน และแสดงให้คุณเห็นว่ามันคือ feature ที่มีประโยชน์และสามารถทำให้การ programing ของคุณเรียบง่ายขึ้นได้อย่างไร

Property Wrapper คือ

Property Wrapper คือการ custom ความสามารถของ property, ด้วยการ ห่อหุ้ม หรือ wrap หรือคือการ wrap logic ของเราเข้าไปอยู่ใน property และเมื่อมีการ stored, read หรือ changed data ต่อ property ที่เราทำการ wrap ไว้ logic ในนั้นก็จะโดน execute ไปด้วย ซึ่งสิ่งนี้จะให้ property มีความสามารถมากขึ้น และ code ดูเรียบง่าย

ดูว่าหมายอย่างเดียวอาจมองไม่เห็นภาพ มา code กันเลยดีกว่า !

Example 1

สร้าง property wrapper ที่เป็น PositiveNumber ที่มี Int ≥ 0 ยกตัวการในการใช้งาน เช่น age

@propertyWrapper
struct PositiveNumber {
private var value: Int
init(wrappedValue: Int) {
value = max(0, wrappedValue)
}
var wrappedValue: Int {
get { value }
set { value = max(0, newValue) }
}
}

// UseCase
struct Person {
@PositiveNumber var age: Int
}
var person = Person(age: 10)
print(person.age) // 10
person.age = -20
print(person.age) // 0

Property wrapper + Generic

Example 2

ตัวอย่างนี้เรานำมาประยุกต์กับ UserDefault

@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}

// UseCase
enum AppSettings {
@UserDefault(key: "LOGIN_STATUS_KEY", defaultValue: false)
static var isLogin: Bool

@UserDefault(key: "USER_TYPE_KEY", defaultValue: "normal")
static var userType: String
}

print("userType \\(AppSettings.userType)") // userType normal
AppSettings.userType = "admin"
print("userType \\(AppSettings.userType)") // userType admin

ในตัวอย่างนี้ property wrapper มี argument 2 ตัวคือ key เป็น string และ defaultValue เป็น generic type (ในที่นี้ความหมายแบบย่อคือ generic คือ type อะไรก็ได้ หมายความว่า defaultValue เป็น Int, String Bool หรือ อื่นๆ) นั้นทำให้ @UserDefault มี defaultValue เป็น dynamic type ด้วย

ProjectedValue

projectedValue เป็นความสามารถหนึ่งของ property wrapper ซึ่งถ้า ให้ค่าใน wrappedValue เป็น main value ค่าใน projectedValue ก็คือ second value โดยเราสามารถใส่ logic ได้เช่นเดียวกัน รู้อย่างนี้แล้วก็ขึ้นอยู่กับ developer แล้วว่าจะ ใส่ logic อะไร แล้วเอาไป apply กับ useCase แบบไหน

struct QuantityView: View {
@State private var quantity = 1
var body: some View {
Stepper("Quantity: \\\\(quantity)",
value: $quantity,
in: 1...99
)
}
}

ตัวอย่างข้างบนมี syntex น่าสนใจ $quantity มันคืออะไรกันนะ… เจอใน SwiftUI บ่อยๆ… มันเรียกว่า projectedValue นั้น

Example 2

@propertyWrapper
struct ImageAsset {
let key: String
init(_ key: String) {
self.key = key
}

func image(for name: String) -> UIImage {
UIImage(named: name) ?? .init()
}

var projectedValue: String {
// add some logic
return key
}

var wrappedValue: UIImage {
self.image(for: key)
}
}
// UseCase
enum Asset {
@ImageAsset("menu_icon") static var menuIcon: UIImage
@ImageAsset("search_icon") static var searchIcon: UIImage
@ImageAsset("settings_icon") static var settingsIcon: UIImage
@ImageAsset("plus_icon") static var plusIcon: UIImage
}

print(Asset.menuIcon) // <UIImage:0x600003004240 anonymous {0, 0} renderingMode=automatic(original)>
print(Asset.$menuIcon) // menu_icon// UseCase

--

--