Awesome KeyPath in Swift
Prerequisite
- Basic knowledge of Swift syntax
- Value semantics and reference semantics
- Generic parameter and type constraints
What I’ll cover
- KeyPath ဆိုတာဘာလဲ
- KeyPath အမျိုးအစားများ
- လက်တွေ့ အသုံးချနိုင်သောနေရာများ
- KeyPath များကို ပေါင်းစပ်အသုံးပြုခြင်း
- နိဂုံး
What is KeyPath
Swift programming language ရဲ့ အဓိက အားသာချက်ကတော့ type safety ဖြစ်ပါတယ်။ KeyPath ဟာ Swift ရဲ့ type safe ဖြစ်မှုကို အသုံးပြုပြီး instance/object တစ်ခုရဲ့ properties တွေကို reference လုပ်နိုင်ပါတယ်။ ဒီအကြောင်းအရာကို စကားနဲ့ပြောတာထက် code ရေးပြတာက ပိုပြီး နားလည်ရလွယ်ကူပါလိမ့်မယ်။
User ဆိုတဲ့ struct တစ်ခုကို ရေးလိုက်ပါတယ်။ User မှာ id, name နဲ့ age ဆိုပြီး properties သုံးခုရှိပါတယ်။
အခုဆိုရင် someone ဆိုတဲ့ variable က User object ကို reference လုပ်လိုက်ပါတယ်။ ဒါက သမားရိုးကျ variable တစ်ခုက instance တစ်ခုကို reference လုပ်ပုံပါ။ KeyPath ကကျတော့ အဲ့ instance type ရဲ့ property ကို reference လုပ်လိုက်လို့ရပါတယ်။ Code example နဲ့ကြည့်လိုက်ရအောင်ပါ။
KeyPath ရဲ့ syntax ကတော့ ‘\’ + ‘TYPE’ + ‘.PROPERTY_OF_THAT_TYPE’ ဖြစ်ပါတယ်။ အခုဆိုရင် ကျွန်တော့်ရဲ့ userNameKeyPath ဆိုတဲ့ variable က User type ရဲ့ name ဆိုတဲ့ property ကို reference လုပ်လိုက်ပါပြီ။ အဲ့လို reference လုပ်တော့ ဘာတွေလုပ်လို့ရလဲဆိုတာ ဆက်ကြည့်ကြရအောင်။
အပေါ်က code ကိုကြည့်မယ်ဆိုရင် keypath ကို subscript syntax နဲ့ ရေးလို့ရတာကို တွေ့ပါလိမ့်မယ်။ Run လိုက်မယ်ဆိုရင်တော့ output က “Kyaw” လို့ ထွက်လာပါလိမ့်မယ်။ ဒါတင်ပဲလားဆိုတော့ မဟုတ်သေးပါဘူး။ Keypath ကိုအသုံးပြုပြီး instance type တစ်ခုရဲ့ writable properties တွေကို data write လုပ်လို့ရပါတယ်။ ကျွန်တော်တို့ရဲ့ ‘someone’ က value semantic (struct) ဖြစ်တဲ့အတွက် ‘let’ ကို ‘var’ လို့ပြောင်းပေးရပါမယ်။ User struct ရဲ့ name property ကတော့ ‘var’ ဖြစ်နေတဲ့အတွက် mutate လုပ်လို့ရပါတယ်။ ‘.’ syntax နဲ့မရေးပဲ keypath ကိုသုံးပြီး data write ကြည့်ပါမယ်။
အပေါ်က code ကို run လိုက်ရင် “Kyaw Monkey” လို့ ထွက်လာပါလိမ့်မယ်။ KeyPath ဆိုတာ ဘာလဲဆိုတာကို နည်းနည်းတော့ သဘောပေါက်လောက်ပြီထင်ပါတယ်။ ရှေ့ဆက်မသွားခင် KeyPath type အကြောင်းကို အရင်လေ့လာကြည့်ရအောင်။
KeyPath<Root, Value>
KeyPath တစ်ခုမှာ Root နဲ့ Value ဆိုပြီး generic parameter နှစ်ခု ပါဝင်ပါတယ်။ Root ဆိုတာကတော့ KeyPath reference လုပ်မယ့် property ရဲ့ root object type (’User’ in our case) ဖြစ်ပြီး Value ဆိုတာကတော့ အဲ့ property value ရဲ့ type (’String’ in our case) ဖြစ်ပါတယ်။
KeyPath အမျိုးအစားများ
အဓိက KeyPath type သုံးမျိုးရှိပါတယ်။
KeyPath<Root, Value>
KeyPath ဆိုတာကတော့ value or reference semantic root type(can be an instance of both struct or class) ရဲ့ read only ပဲရတဲ့ property တွေကို reference လုပ်တဲ့ type ပါ (Eg- \User.id)
WritableKeyPath<Root, Value>
WritableKeyPath ကတော့ value semantic root type (struct) ရဲ့ read, write ရတဲ့ property ကို reference လုပ်တဲ့ type ပါ (Eg- our ‘userNameKeyPath’ variable)
ReferenceWritableKeyPath<Root, Value>
ReferenceWritableKeyPath ကတော့ reference semantic root type (object) ရဲ့ read, write ရတဲ့ property ကို reference လုပ်တဲ့ type ပါ။
ဒါ့အပြင် ပိုပြီး flexible ဖြစ်တဲ့ KeyPath type နှစ်မျိုးရှိပါတယ်။
PartialKeyPath<Root>
PartialKeyPath ကတော့ root type ကိုသိရုံနဲ့ root ရဲ့ property တွေအကုန်လုံးကို ယူလို့ရပါတယ်။
AnyKeyPath
AnyKeyPath ကတော့ နာမည်အတိုင်း type erase လုပ်ထားတဲ့ key path ဖြစ်ပါတယ်။ ဘယ်လို key path အမျိုးအစားမဆို ဝင်လာလို့ရပါတယ်။
KeyPath တွေကို type cast လည်း ပြန်လုပ်လို့ရပါတယ်။
အပေါ်က code မှာဆိုရင် type erase လုပ်ထားတဲ့ keyPaths array ထဲကနေမှ KeyPath<User, Int> အဖြစ် ပြန် type cast လုပ်ယူပြီး age ကို print ထုတ်ထားပါတယ်။ ဒီနေရာမှာ print ထုတ်ရုံထုတ်တာမို့ KeyPath type ကို အလွယ်တကူ သုံးလိုက်တာပါ။ တစ်ကယ်လို့ value ကိုပါ ပြောင်းချင်တယ်ဆိုရင်တော့ WritableKeyPath အနေနဲ့ type cast လုပ်ရပါလိမ့်မယ်။
KeyPath in real-life
KeyPath ကို (Root) ->(Value) → Void ဆိုတဲ့ function တစ်ခုအနေနဲ့ မြင်ကြည့်လို့ရပါတယ်။ Object type နဲ့ အဲ့ object type ကနေမှတစ်ဆင့် property ကိုထုတ်ပေးပြီး Void ကို return ပြန်တဲ့ function တစ်ခုဆိုလည်း မမှားပါဘူး။ Swift က KeyPath တွေကို function တွေအဖြစ် အလိုလျှောက် ပြောင်းပေးနိုင်ပါတယ်။
အပေါ်က code ကိုကြည့်ရင် နားလည်မယ် ထင်ပါတယ်။ Map က (Element) throws -> T ဆိုတဲ့ closure ကို parameter အနေနဲ့ လက်ခံပါတယ်။ ကျွန်တော်က Element ရဲ့ keypath ကို ထည့်ပေးလိုက်တဲ့အခါ သူ့ဘာသူ element ကနေ keypath ကိုသုံးပြီး property ကို access လုပ်တဲ့ func တစ်ခုအဖြစ်် အလိုလိုပြောင်းပေးသွားတာဖြစ်ပါတယ်။
ဒီတစ်ခါ ခုနက users array ကိုပဲ အသက်အလိုက် sort လုပ်ကြည့်ပါမယ်။
အပေါ်က code ကိုပဲ KeyPath သုံးပြီး ဖတ်ရပိုရှင်းအောင် ရေးလို့ရပါတယ်။
- Sorting ကို ငယ်စဉ်ကြီးလိုက်သွားမလား ကြီးစဉ်ငယ်လိုက်သွားမလားဆိုတာကို ဆုံးဖြတ်နိုင်ဖို့ SortingStyle ဆိုတဲ့ enum type တစ်ခုကို ရေးလိုက်ပါတယ်။
- Enum case တစ်ခုဆီမှာ key path ကို associated value အနေနဲ့ လက်ခံထားပါတယ်။
- Sequence extension တစ်ခု ရေးလိုက်ပါတယ်။ (Array, Set စတဲ့ collection တွေအားလုံးဟာ sequence type တွေဖြစ်ပါတယ်)
- ကျွန်တော်တို့ရဲ့ custom sorted func ကို ရေးလိုက်ပါတယ်။ ဒီနေရာမှာ T: Comparable ကို ဘာကြောင့်သုံးရသလဲဆိုတော့ ကျွန်တော်တို့ key path ရဲ့ property type သည် comparable type ဖြစ်မှ ‘<’, ‘>’, ‘==’ စတဲ့ operator တွေကို သုံးလို့ရမှာ ဖြစ်ပါတယ်။ Compare လုပ်လို့မရတဲ့ type တွေကို sort ဖို့ရာ မဖြစ်နိုင်ပါဘူး။
- Switch ကိုသုံးပြီး ascending လား descending လားဆိုတာကို ဆုံးဖြတ်ပါတယ်။
- နောက်ဆုံးမှာတော့ standard library ရဲ့ sorted func နဲ့ သက်ဆိုင်ရာ order အတိုင်း sort လုပ်လိုက်ပါတယ်။
နောက်ထပ် ဉပမာတစ်ခုကို ရေးကြည့်ပါမယ်။
Network ပေါ်ကနေ user တွေကို fetch လုပ်တဲ့ fake func တစ်ခုရေးထားပါတယ်။ တစ်စက္ကန့်ကြာတော့မှ completion handler ထဲကို ရှေ့မှာရေးထားတဲ့ users array ကို ထည့်ပေးလိုက်ပါတယ်။ ViewController ထဲမှာတော့ fetchUsers func ကို ပြန်ခေါ်ထားပြီး data ရလာတဲ့အခါ users ဆိုတဲ့ property ကို data feed လုပ်ပါတယ်။ ဒီ code ကို သေချာပြန်ကြည့်မယ်ဆိုရင် [User] ကို view controller ရဲ့ property မှာသွား set လုပ်တာကို တွေ့ရပါမယ်။ ဒီဟာကို ပိုပြီး ဖတ်ရလွယ်အောင် key path သုံးပြီး ရေးကြည့်ပါမယ်။
fetchUsers ရဲ့ closure ထဲကို pass လုပ်ပေးရမယ့် func type ဟာ ([Users]) → Void ဖြစ်ပါတယ်။ ကျွန်တော်တို့က key path ကိုသုံးပြီး view controller ရဲ့ property ထဲကို [Users] set လုပ်ချင်တာဖြစ်တဲ့အတွက် reference writable keypath နဲ့ အဲ့ key path အတွက် root object ကို parameter အနေနဲ့ လက်ခံဖို့ လိုအပ်ပါတယ်။ ပြီးရင် fetchUsers closure ကလက်ခံတဲ့ func type တစ်ခုကို return ပြန်ပေးဖို့ လိုအပ်ပါတယ်။
- Generic func setter ကိုရေးလိုက်ပါတယ်။ လိုအပ်တဲ့ Root နဲ့ Value type နှစ်မျိုးကို generic parameter အနေနဲ့ယူထားပါတယ်။ ဒီ func ကို view controller တွေမှာပဲ သုံးစေချင်တာဖြစ်တဲ့အတွက် Root: AnyObject ဆိုပြီး root type သည် class ဖြစ်ရမယ်လို့ constraint လုပ်လိုက်တာဖြစ်ပါတယ်။ ပြီးရင်တော့ key path ကို subscript လုပ်နိုင်မယ့် root object နဲ့ key path object ကို parameter အနေနဲ့ယူပြီး closure ထဲကိုထည့်ဖို့ func signature တူညီတဲ့ (Value) → Void ကို return ပြန်ပေးလိုက်ပါတယ်။
- Function ထဲမှာတော့ closure တစ်ခုရေးထားပြီး root object ကို weak အနေနဲ့ capture လုပ်လိုက်ပါတယ်။
- Closure ထဲကို pass လုပ်ပေးလိုက်မယ့် value ကို key path subscript syntax ကိုသုံးပြီး root object ရဲ့ property မှာ set လုပ်လိုက်ပါတယ်။
- ဒါဆိုရင်တော့ fetchUsers func ထဲကို ကျွန်တော်တို့ရေးထားတဲ့ setter func ကို pass လုပ်ပေးလိုက်လို့ရပါပြီ။
အခု example မှာပဲ user type အမျိုးမျိုးရှိတယ် ဆိုကြပါစို့။ Silver user, Gold user, Platinum user ဆိုပြီး ရှိတယ်ပေါ့။ ဒီနေရာမှာ table cell ထဲကို ဝင်လာမယ့် object type တွေက User တစ်မျိုးတည်းမဟုတ်တော့ဘဲ SilverUser, GlodUser, PlatinumUser ဆိုပြီး သုံးမျိုးဝင်လာနိုင်တယ်။
ကျွန်တော်တို့ရဲ့ cell က ဘယ်လို data တွေကို လိုအပ်သလဲဆိုတော့ user name, user type နှစ်မျိုး ကို UI နဲ့ data bind ပေးရမယ်။
User Type သုံးမျိုးအတွက် cell နဲ့ data bind တဲ့ func သုံးခုတောင် ရေးထားရပါတယ်။ ဒါကို ရှောင်ချင်ရင် protocol ထုတ်ရေးလိုက်လည်း ရပါတယ်။
ဒါပေမယ့် ဒီလိုမျိုး အခြား cell တွေမှာ multiple object လက်ခံချင်တယ်ဆိုရင် data providing protocol type တစ်ခု introduce လုပ်, လက်ရှိ model တွေကို သက်ဆိုင်ရာ cell အတွက် provider အဖြစ်ပြောင်း အလုပ်ရှုပ်လှပါတယ်။ KeyPath ကိုသုံးပြီးတော့လည်း abstraction ကျကျ ရေးလို့ရနိုင်ပါတယ်။ ပြန်စဉ်းစားကြည့်မယ်ဆို အပေါ်က configureCell func မှာ သိဖို့လိုအပ်တာ userName နဲ့ userType ဆိုတဲ့ String နှစ်ခုကို လိုချင်တာပါ။ ဘယ် object ရဲ့ String မဆို ရပါတယ်။
- CellConfigurator struct တစ်ခု ထုတ်ရေးလိုက်ပါတယ်။ ဒီထဲမှာ UserCell ကို data bind ဖို့လိုအပ်တဲ့ key path တွေကို ယူထားပြီး model ကိုတော့ ကြိုက်တဲ့ model type လာဆိုတဲ့ပုံစံနဲ့ ရေးထားတာကို တွေ့ရပါလိမ့်မယ်။
- configureCell func ထဲမှာတော့ key path ကို သုံးပြီး UI ကို data set လုပ်ပါတယ်။
- ပြီးရင် view controller ထဲကနေ cell.configure ဆိုပြီး ကိုယ် configure လုပ်လိုတဲ့ model နဲ့ data bind ရမယ့် property တွေကို key path အနေနဲ့ ထည့်ပေးလိုက်ပါတယ်။
KeyPath Composition
ကျွန်တော်တို့ရဲ့ model တော်တော်များများဟာ flat မဖြစ်နေပါဘူး။ Model တစ်ခုမှာ နောက်ထပ် model တစ်ခုကို nested store လုပ်ထားလေ့ရှိပါတယ်။ ရှေ့က User type မှာ Address ဆိုတဲ့ type တစ်ခု ထပ်တိုးလိုက်ရအောင်ပါ။
User ထဲမှာ address ဆိုတဲ့ property တစ်ခု တိုးလာပါပြီ။ Address ထဲက city ကို key path composition နဲ့လှမ်းယူကြည့်ပါမယ်။
- Demo ပြနိုင်ဖို့ user object တစ်ခု ဆောက်ပါတယ်။
- User ထဲက address ကို key path နဲ့ လှမ်းယူပါတယ်။
- Address ထဲက city ကို key path နဲ့ လှမ်းယူပါတယ်။
- cityKeyPath ကို addressKeyPath ထဲ append လုပ်လိုက်ပါတယ်။
- ပြီးရင်တော့ user object ထဲက city ကို compose လုပ်ထားတဲ့ userCityKeyPath ကိုသုံးပြီး လှမ်း access လုပ်လိုက်ပါတယ်။
ဒီနေရာမှာ ပြောစရာရှိတာက ရိုးရိုး ‘.’ နဲ့ ခေါ်ရင်လဲရရဲ့သားနဲ့ ဘာကြောင့် appending(path:) တွေသုံးပြီး ရှုပ်ရှုပ်ထွေးထွေး လုပ်ရတာလဲပေါ့။ ကျွန်တော်တို့ မြန်မာစကားမှာ အခုရှုပ်မှ နောင်ရှင်းဆိုတဲ့ စကားရှိပါတယ်။ ဆိုကြပါစို့။ ကျွန်တော်တို့ app မှာ nested ဖြစ်တဲ့ object type အမျိုးမျိုးနဲ့ array တွေရှိတယ်။ အဲ့တာကိုမှ UI မှာ flat ဖြစ်ဖြစ် ပြန်ပြလိုတဲ့အခါ ကျွန်တော်တို့ key path ကို သုံးပြီး recursive call တွေနဲ့ အောက်ဆုံးက လိုချင်တဲ့ path အထိရောက်အောင်သွားပြီး flat ဖြစ်အောင် data manipulate လုပ်လို့ရပါတယ်။ ဒါကို manually ရေးမယ်ဆိုရင်လည်း ရပေမယ့် မလိုအပ်ပဲ protocol အသစ်တွေ introduce ရတာ readability ကျတာ စတာတွေဖြစ်နိုင်ပါတယ်။
နိဂုံး
KeyPath ဟာ type-safe ဖြစ်ဖြစ်နဲ့ object type အပေါ် မှီခိုမှုမရှိဘဲ မည်သည့် object ရဲ့ property ကိုမဆို လှမ်းပြီးယူတာ data write တာ လုပ်လို့ရပါတယ်။ ဒါ့အပြင် protocol တွေနဲ့ လုပ်ယူလို့မရတဲ့ dynamic feature တွေ abstraction level တွေကို လုပ်ပေးနိုင်ပါတယ်။ အပေါ်က ဉပမာတွေကို ပြန်ကြည့်မယ်ဆိုရင် ကျွန်တော်တို့ရဲ့ code က parent class မလိုပဲ polymorphic ဖြစ်တဲ့ code တွေကို ရေးသားနိုင်တာကို တွေ့ရပါလိမ့်မယ်။ Apple ကိုယ်တိုင်ကလည်း key path ကို property wrapper တွေမှာ သုံးထားတာ တွေ့ရပါမယ် (SwiftUI ရဲ့ environment, core data property wrapper တွေကို ကြည့်ပါ)။ Syntax အရလည်း expressive ဖြစ်သလို property type တစ်ခုတည်းကိုကွက်ပြီး dynamically reference လုပ်လို့ရတယ်ဆိုတာဟာ အင်မတန် powerful ဖြစ်တဲ့ language feature တစ်ခုဖြစ်ပါတယ်။