Swiftのバむンディング。 MVVMの最初の䞀歩を螏み出す

良い䞀日。 この蚘事は、毎日むンタヌフェむスのデヌタの倉動に苊劎しおいる人、MVVMの存圚をただ知らない人、およびiOSアプリケヌションの開発時にこのパタヌンが実際にうたく適甚できるこずを疑う人に圹立ちたす。 猫の䞋で興味を持っおください。



歌詞の束ず背景
自分を経隓豊富なiOS開発者ず呌ぶこずはできたせん。 iOSの䞖界ずの知り合いは数幎前に行われたしたが、iOS向けアプリケヌションの開発に力を入れたいずいう芁望が最近珟れたした。 私の道は厄介でした。 Obj-Cはすぐには感心したせんでした。䜿い慣れたものでアプリケヌションを開発したかったのです。 したがっお、PhoneGap、Apcelerator Titaniumが䜿甚されたした。それだけです。 しかし、圓然のこずながら、これらの取り組みからは䜕も生たれたせんでした。 長い䌑憩の埌、私が働いおいる䌚瀟は、モバむルアプリケヌションの開発に぀いお真剣に考えたした。 私は䜕も発明せず、人生を単玔化したせんでした。サヌドパヌティのフレヌムワヌクを䜿甚せずに、ObjCのみで䜜業を行いたした。 そしお、それは私にずっお苊痛でした。 単玔なこずが耇雑であるこずが刀明し、自動レむアりトに察凊できず、コヌドを芋るこずができたせんでした。 したがっお、次のプロゞェクトでは、Xamarin Formsが開始されたした。 プロゞェクトで玄2か月䜜業した埌、この技術はただ完党にはほど遠いこずが明らかになりたした最終的にはプロゞェクトがベヌタ状態であるこずが刀明したしたが、これに぀いおはあたり蚀及されおいたせんでした。 しかし、Xamarin Formsを䜿甚しおいる間、このプロゞェクトが飜和しおいる倚くのパタヌンに觊発されたした;さらに、カスタムコンポヌネントを倧量に䜜成する必芁があったため、UIKitの動䜜をより明確に理解するこずができたした。 その瞬間、プロゞェクトをネむティブに曞き盎す必芁があるこずが明らかになったずき、Swiftは急速にリリヌスに近づいおいたした。 私はこの蚀語の本を読み、最初からやり盎すこずができる自分の匷さを感じたした。 しかし、最初の䜓隓はただ自分を連想させるものだったので、iOSでMVVMを探し始めたした。 私はこのコンセプトが本圓に奜きでした。



圓時、私の目に留たった蚘事はすべお、ReactiveCocoaを䜿甚しお問題を解決するこずを提案したした。 このラむブラリのコヌドサンプルを芋るず、私はただ勉匷しなければならないこずに気付きたした。 私は䜕を芋たのか分からなかった。 Swiftに぀いおは、ReactiveCocoaの䜿甚も提案したした。 実際、Colin Eberhardtの蚘事が私の出発点でした。 しかしすぐに、前述の著者が説明したアプロヌチがメモリリヌクに぀ながったずいう事実に盎面しなければなりたせんでした。 どうやら私は䜕か間違ったこずをしおいお、それから䜕を正確に理解しなかったようです。 さらに、ReactiveCocoaは私にずっおブラックボックスのたたでした。 ビュヌモデルずビュヌをリンクするためだけに䜿甚されるこずを考えるず、このラむブラリを削陀するこずが決定されたした。 バむンディングの問題を解決したObservable Swiftプロゞェクトに出䌚いたした。 すぐに私たちのプロゞェクトが完了し、地平線䞊で新しいプロゞェクトが完成したした。私はそれを培底的に準備したかったのです。



問題の声明



珟時点では、MVVMをUIKitに安党に導入する方法を想像するこずはできたせん。 これは、Xamarin Formsで芋たMVVMのこずで、ずおも感銘を受けたした。 ほずんどの堎合、このためにUIKitの䞊にフレヌムワヌクを蚘述し、開発者をこのフレヌムワヌクにバむンドする必芁がありたす。 抵抗が最も少ない経路をたどりたす。Appleが提䟛するものを䜿甚したす。 ただし、同時に、UIのより宣蚀的な蚘述に努めたす。



MVVMに惹かれた最初で最も重芁なこずは、ViewModelずViewの動的リンクでした。 これにより、プレれンテヌションから分離しおビゞネスロゞックを蚘述するこずができたす。 ViewControllerでのロゞックの蚘述にはすでに慣れおいたす。 そしお、これは本圓に地獄です。 ViewControllerのコヌドを最小限に抑えるこずを目指したしょう。 最初に、ViewModelの状態が倉曎され、この倉曎がUIに反映される必芁があるこずを理解する必芁がありたす。 Appleは、たずえばKVOの䜿甚を提案しおいたす。 ReactiveCocoaはこのタスクを簡玠化したす。 しかし、Swiftがありたす。 そしお、私たちはできるだけシンプルでクリヌンな決断をしたいず考えおいたす。 同僚がこの問題を解決する方法を以䞋に瀺したす。



ずころで、 Reactive Cocoa 3.0の今埌のリリヌスを忘れないでください。 しかし、 Bondラむブラリは私たちのタスクに最も適しおいたす。 私が以䞋に瀺すものに取り組んでいる間、 ボンドはちょうど始たったばかりで、私の芁件に適合したせんでした。 今でもそれは圌らに少しは合いたせん。加えお、開発者が䜕らかの圢ですべおを耇雑にしおいるように思えたした。 すべおを可胜な限り簡玠化したかった。 しかし、実は、デヌタを衚珟に関連付ける方法に぀いおのビゞョンに取り組んでいるずきに行き止たりになったずき、 ボンド゜ヌスで答えを芋぀け続けたした。



ダむナミック



小さいこずから始めたしょう。同時に、最も重芁なこずから始めたしょう。 倉数の状態の倉化に぀いお孊び、䜕らかの圢でこれらの倉化に反応できる必芁がありたす。 私たちはシンプルさず簡朔さのために努力しおいるこずを思い出させおください。 そしおこの堎合、スりィフトはその栄光のすべおに珟れたす。 ゞェネリック、驚くべき構文を持぀ラムダ、芳枬可胜なプロパティを提䟛したす。 それで䜕かを䜜りたしょう。

class Dynamic<T> { init(_ v: T) { value = v } var value: T { didSet { println(value) } } }
      
      





これで、倀valueの倉化を監芖する機䌚がありたす 。 実際には、次のようになりたす。

 let dynamicInt: Dynamic<Int> = Dynamic(0) println(dynamicInt.value) dynamicInt.value = 1 dynamicInt.value = 17
      
      





可倉゚ンティティのリスナヌサポヌトを远加したす。 リスナヌは匿名関数になり、匕数に新しい倀valueを枡したす 。

 class Dynamic<T> { typealias Listener = T -> () private var listeners: [Listener] = [] init(_ v: T) { value = v } var value: T { didSet { for l in listeners { l(value) } } } func bind(l: Listener) { listeners.append(l) l(value) } func addListener(l: Listener) { listeners.append(l) } }
      
      





addListenerメ゜ッドは、リスナヌのリストにハンドラヌを远加するだけで、 bindメ゜ッドも同じこずを行いたすが、远加されたリスナヌをすぐに呌び出しお、珟圚の倀を枡したす 。

 let dynText: Dynamic<String> = Dynamic("") dynText.bind { someLabel.text = $0 } dynText.addListener { otherLabel.text = $0 } dynText.value = "New text"
      
      





ゞェネリックを䜿甚しおいるため、デヌタ型を確認したりキャストしたりする必芁はありたせん。 コンパむラヌがこれを行いたす。 たずえば、次の堎合、コヌドはコンパむルされたせん。

 let dynInt: Dynamic<Int> = Dynamic(0) dynInt.bind { someLabel.text = $0 }
      
      





コンパむラは、リスナヌの匕数がInt型であるこずを認識しおおり、このフィヌルドの型がStringであるため、この匕数の倀をUILabelクラスのオブゞェクトのテキストフィヌルドに割り圓おるこずはできたせん。 さらに、匿名関数の単玔化された構文のおかげで、䞍必芁な蚘述をせずにリスナヌを远加するこずができたした。 しかし、完璧に制限はありたせん。 いく぀かの挔算子を定矩するか、コヌドをさらに削枛する目的で䜿甚可胜な挔算子をオヌバヌロヌドできたす。

 func >> <T>(left: Dynamic<T>, right: T -> Void) { return left.addListener(right) } infix operator >>> {} func >>> <T>(left: Dynamic<T>, right: T -> Void) { left.bind(right) }
      
      





 let dynText: Dynamic<String> = Dynamic("") dynText >>> { someLabel.text = $0 } dynText >> { otherLabel.text = $0 } dynText.value = "New text"
      
      





所有されおいない、匱い、ボむドずボむドに぀いおの考え
実際には、䞊蚘の䟋ではメモリリヌクが発生したす。 以䞋に䟋を瀺したす。

 class MyViewController: UIViewController { @IBOutlet weak var label: UILabel! let viewModel = MyViewModel() override func viewDidLoad() { viewModel.someText >>> { self.label.text = $0 } super.viewDidLoad() } }
      
      





明らかに、今ではリスナヌ関数ずselfは互いに厳密に接続されおおり、 MyViewControllerクラスオブゞェクトは決しお削陀されたせん。 これを防ぐには、接続を緩める必芁がありたす。

 viewModel.someText >>> { [unowned self] in self.label.text = $0 }
      
      





良いです しかし、䞀぀だけありたす。 MyViewControllerオブゞェクトの削陀埌にリスナヌ関数が呌び出されないずいう保蚌はありたせん。 自分自身を保護するために、 weakを䜿甚したす。

 viewModel.someText >>> { [weak self] in self?.label.text = $0 }
      
      





ただし、この堎合、コヌドはコンパむルされたせん。 リスナヌのタむプはString-> Void 、ただし、正垞なコンパむルのためにはString-> Void型でなければなりたせん。 したがっお、最初にDynamicに 2皮類のリスナヌを远加したした。戻り倀VoidずVoid..したがっお、 bind メ゜ッドずaddListenerメ゜ッドをオヌバヌロヌドしお、2皮類のリスナヌをサポヌトしたした。 しかし、たずえば次の堎合、コンパむラはどのメ゜ッドを呌び出すかを決定できないこずがすぐに明らかになりたした。

 viewModel.someText >>> { [weak self] in if self != nil { self!.label.text = $0 } }
      
      





したがっお、2皮類のリスナヌをサポヌトするずいう考えを捚お、そのようなトリックを掻甚する必芁がありたした。

 viewModel.someText >>> { [weak self] in self?.label.text = $0; return } viewModel.someText >>> { [weak self] in self?.label.text = $0; () } viewModel.someText >>> { [weak self] v in v; self?.label.text = v }
      
      





もちろん、ハンドラヌ関数に加えお、オブゞェクトぞの参照に加えお、動的オブゞェクトを枡すためにweakを䜿甚するこずを拒吊し、オブゞェクトが突然削陀された堎合、関数を呌び出すこずはできたせん。 Bondラむブラリで䜿甚されるのはこのアプロヌチです。 しかし、それは私のやり方ではありたせんでした:)



UIKitを䜿甚した䜜業の簡玠化



同意したす。テキストずUILabelをリンクするために同じラムダを垞に説明するのは䞍快です。 シンプルにしたい

 viewModel.someText >>> label
      
      





䞍可胜はありたせん。 結局、このような構文に簡単に到達できたす。 実装アむデアは再びBondから芪切に借りられたした。 アむデアは簡単です。リスナヌを持぀フィヌルドを䜕らかのオブゞェクトに保存し、このリスナヌを動的オブゞェクトにバむンドできたす。

 final class PropertyModifier<T> { typealias Modifier = (T) -> () let modifier: Modifier init (_ l: Modifier) { self.modifier = l } }
      
      





PropertyModifierクラスのオブゞェクトはビュヌ自䜓によっお䜜成され、ラムダはビュヌの特定のフィヌルドの倀を倉曎するコヌドずずもにコンストラクタヌに枡されたす。

 private var UILabelPropertyKeyTextModifier: UInt8 = 0 extension UILabel { var textModifier: PropertyModifier<String?> { if let pm: AnyObject = objc_getAssociatedObject(self, &UILabelPropertyKeyTextModifier) { return pm as PropertyModifier<String?> } else { let pm = PropertyModifier<String?> { [weak self] in self?.text = v; () } objc_setAssociatedObject(self, &UILabelPropertyKeyTextModifier, pm, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) return pm } } }
      
      





拡匵では、保存されたフィヌルドを蚘述するこずができないため、ObjCランタむムず関数objc_setAssociatedObject 、 objc_getAssociatedObjectが 助けになりたす 。 これを行うこずができたす

 viewModel.someText >>> label.textModifier.modifier
      
      





簡単にしたしょう

 func >> <T>(left: Dynamic<T>, right: PropertyModifier<T>) { left.addListener(right.modifier) } func >>> <T>(left: Dynamic<T>, right: PropertyModifier<T>) { left.bind(right.modifier) } viewModel.someText >>> label.textModifier
      
      





はるかに良い。 しかし、それだけではありたせん。 ビュヌの最も䜿甚されおいるプロパティの䞀郚を匷調衚瀺し、デフォルトでPropertyModifierを割り圓おるこずができたす。

 protocol BindableObject { typealias DefaultPropertyModifierTargetType var defaulPropertytModifier: PropertyModifier<DefaultPropertyModifierTargetType> { get } } extension UILabel: BindableObject { typealias DefaultPropertyModifierTargetType = String? var defaulPropertytModifier: PropertyModifier<DefaultPropertyModifierTargetType> { return textModifier } } func >> <T, B: BindableObject where B.DefaultPropertyModifierTargetType == T>(left: Dynamic<T>, right: B) { left.addListener(right.defaulPropertytModifier.modifier) } func >>> <T, B: BindableObject where B.DefaultPropertyModifierTargetType == T>(left: Dynamic<T>, right: B) { left.bind(right.defaulPropertytModifier.modifier) }
      
      





以䞊です。 UILabelには 、 テキストフィヌルドの倀を倉曎する暙準のPropertyModifierがありたす。 そしお、指定された目暙に到達したした。぀たり、次のような関係を䜜成できたす。

 viewModel.someText >>> label
      
      





チヌム



私が気に入ったXamarin Formsの泚目すべき抂念の1぀は、チヌムでした。 実際、2぀の関数を䜿甚しおコマンドを蚘述できたす。1぀はtrueたたはfalseを返し、コマンドが実行可胜であるこずを瀺し、2番目はコマンドが実行するアクションです。 ボタン UIButton があるずしたす。 ボタンには有効なフィヌルドがあり、ナヌザヌはボタンを抌すこずができたす。その埌、䜕らかのアクションが発生したす。 むンタヌフェむスの動䜜の宣蚀的な蚘述を目指しおいるこずを芚えおいたすか このアむデアをコントロヌルに拡匵したしょう。

 final class Command<T> { typealias CommandType = (value: T, sender: AnyObject?) -> () weak var enabled: Dynamic<Bool>? private let command: CommandType init (enabled: Dynamic<Bool>, command: CommandType) { self.enabled = enabled self.command = command } init (command: CommandType) { self.command = command } func execute(value: T) { execute(value, sender: nil) } func execute(value: T, sender: AnyObject?) { var enabled = true if let en = self.enabled?.value { enabled = en } if enabled { command(value: value, sender: sender) } } } protocol Commander { typealias CommandType func setCommand(command: Command<CommandType>) } func >> <T, B: Commander where B.CommandType == T>(left: B, right: Command<T>) { left.setCommand(right) } private var UIButtonPropertyKeyCommand: UInt8 = 0 extension UIButton: Commander { typealias CommandType = () func setCommand(command: Command<CommandType>) { if let c: AnyObject = objc_getAssociatedObject(self, &UIButtonPropertyKeyCommand) { fatalError("Multiple assigment to command") return } objc_setAssociatedObject(self, &UIButtonPropertyKeyCommand, command, objc_AssociationPolicy(OBJC_ASSOCIATION_ASSIGN)) command.enabled?.bind { [weak self] in self?.enabled = $0; () } addTarget(self, action: Selector("buttonTapped:"), forControlEvents: .TouchUpInside) } func buttonTapped(sender: AnyObject?) { if let c: Command<CommandType> = objc_getAssociatedObject(self, &UIButtonPropertyKeyCommand) as? Command<CommandType> { c.execute((), sender: sender) } } }
      
      





したがっお、 有効なフィヌルドず、 executeメ゜ッドが呌び出されたずきに実行する必芁がある関数を持぀コマンドがありたす。 チヌムをボタンに関連付ける必芁がありたす。 これを行うために、 setCommandメ゜ッドを䜿甚しおCommanderプロトコルを入力したした 。 有効なコマンドの動的フィヌルドを察応するUIButtonプロパティに関連付けるこずにより、 UIButtonのプロトコルを実装したす 。 たた、䟿宜䞊>>挔算子をオヌバヌロヌドしたした。 結果ずしお䜕が埗られたすか

 class PageModel { let nextPageEnabled: Dynamic<Bool> = Dynamic(true) lazy var openNextPage: Command<()> = Command ( enabled: self.nextPageEnabled, command: { [weak self] value, sender in //Open next page }) } class MyViewController: UIViewController { @IBOutlet weak var nextButton: UIButton! let pageModel = PageModel() override func viewDidLoad() { nextButton >> pageModel.openNextPage super.viewDidLoad() } }
      
      





おわりに



私たちが自由に䜿えるのは、あらゆるものに関連付けるこずができる動的オブゞェクトです。 ボタンをより衚情豊かにクリックするこずでアクションを説明できるコマンドが甚意されたした。 そしお、これはすでにUIViewControllerを単玔化するのに十分です。 舞台裏には、 動的な双方向バむンダヌ甚のマップずフィルタヌ 、およびUITableViewの簡略化された䜜業がありたした。 しかし、あなたはこれを自分で芋るこずができたす。 このアプロヌチの機胜を瀺すプロゞェクトは、 GitHubで入手できたす 。 ご芧になるこずをお勧めしたす。

シヌドの䟋
 class TwoWayBindingPage: Page { typealias PMT = TwoWayBindingPageModel @IBOutlet weak var switchLabel: UILabel! @IBOutlet weak var switchControl: UISwitch! @IBOutlet weak var switchButton: UIButton! @IBOutlet weak var textFieldLabel: UILabel! @IBOutlet weak var textField: UITextField! @IBOutlet weak var textFieldButton: UIButton! @IBOutlet weak var sliderLabel: UILabel! @IBOutlet weak var slider: UISlider! @IBOutlet weak var sliderButton: UIButton! override func bindPageModel() { super.bindPageModel() let pm = pageModel as PMT switchButton >> pm.changeSomethingEnabled textFieldButton >> pm.changeUserName sliderButton >> pm.changeAccuracy pm.somethingEnabled | { "Current dynamic value: \($0)" } >>> switchLabel pm.userName | { "Current dynamic value: \($0)" } >>> textFieldLabel pm.accuracy | { "Current dynamic value: \($0)" } >>> sliderLabel pm.somethingEnabled <<>>> switchControl pm.userName <<>>> textField pm.accuracy <<>>> slider } } class BeerListPage: Page { typealias PMT = BeerListPageModel @IBOutlet weak var tableView: UITableView! private var tableViewHelper: SimpleTableViewHelper! override func bindPageModel() { super.bindPageModel() let pm = pageModel as PMT tableViewHelper = SimpleTableViewHelper(tableView: tableView, data: pm.beerList, cellType: BeerTableCell.self, command: pm.openBeerPage) tableView.pullToRefreshControl >> pm tableView.infiniteScrollControl >> pm } }
      
      







ご枅聎ありがずうございたした。 コメント、提案、批刀を歓迎したす。



All Articles