Swift Properties Wrapper คือ
เชื่อว่า 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