.sender,

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

!== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

!== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach {

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from:

Staatiliste mustritega töötamine: kiire MVVM-i õpetus



Täna näeme, kuidas meie kasutajate uued tehnilised võimalused ja ootused reaalajas andmepõhistele rakendustele tekitavad uusi väljakutseid meie programmide, eriti mobiilirakenduste struktuuris. Kuigi see artikkel räägib ios ja Kiire , on paljud mustrid ja järeldused võrdselt rakendatavad nii Androidi kui ka veebirakenduste jaoks.

Moodsate mobiilirakenduste toimimises on viimase paari aasta jooksul toimunud oluline areng. Tänu laialdasemale Interneti-juurdepääsule ja sellistele tehnoloogiatele nagu tõukemärguanded ja WebSocketid pole kasutaja enam paljudes tänapäevastes mobiilirakendustes enam ainus käitamisallikate allikas - ja mitte tingimata kõige olulisem.



Vaatame lähemalt, kui hästi töötab kaks Swifti kujundusmustrit koos moodsa vestlusrakendusega: klassikaline mudeli-vaate-kontrolleri (MVC) muster ja lihtsustatud muutumatu mudeli-vaate-vaate mudeli muster (MVVM, mõnikord stiliseeritud “ViewModeli muster”) ”). Vestlusrakendused on hea näide, kuna neil on palju andmeallikaid ja nad peavad andmete saamisel oma kasutajaliideseid mitmel erineval viisil värskendama.



Meie vestlusrakendus

Rakendusel, mida selles Swifti MVVM-i õpetuses juhendina kasutame, on enamus põhifunktsioone, mida teame sellistest vestlusrakendustest nagu WhatsApp. Vaatame üle funktsioonid, mida juurutame, ja võrdleme MVVM-i ja MVC-d. Rakendus:



Selles demorakenduses pole reaalset API, WebSocket'i või põhiandmete juurutamist, mis muudaks mudeli juurutamise veidi lihtsamaks. Selle asemel lisasin vestlusroboti, mis hakkab teile vastama, kui alustate vestlust. Kõiki muid marsruute ja kõnesid rakendatakse nii, nagu oleks, kui salvestusruum ja ühendused oleksid reaalsed, kaasa arvatud väikesed asünkroonsed pausid enne tagasipöördumist.

Ehitatud on järgmised kolm ekraani:



Ekraanid Vestlusloend, Loo vestlus ja Sõnumid.

Klassikaline MVC

Kõigepealt on iOS-i rakenduse loomiseks tavaline MVC-muster. Nii struktureerib Apple kogu oma dokumentatsioonikoodi ning viisi, kuidas API-d ja kasutajaliidese elemendid loodavad töötada. Seda õpetatakse enamikele inimestele, kui nad läbivad iOS-i kursuse.



Sageli süüdistatakse MVC-d mõnes tuhandes koodireas ülespuhutud UIViewController s. Kuid kui seda rakendada hästi, siis on iga kihi vahel hea eraldatus, võib meil olla üsna õhuke ViewController s, mis toimivad ainult vahehalduritena View s, Model s ja muude Controller s.

Siin on vooskeem rakenduse MVC juurutamine (jättes selguse huvides välja CreateViewController):



MVC juurutamise vooskeem, jättes selguse huvides välja CreateViewController.

Vaatame kihid üksikasjalikult üle.



Mudel

Mudelikiht on tavaliselt MVC-s kõige vähem probleemne kiht. Sel juhul otsustasin kasutada ChatWebSocket, ChatModel ja PushNotificationController vahendama Chat ja Message objektid, välised andmeallikad ja ülejäänud rakendus. ChatModel on tõe allikas rakenduses ja töötab ainult selles demorakenduses mälus. Tõsielus olevas rakenduses toetaks seda tõenäoliselt põhiandmed. Lõpuks ChatEndpoint haldab kõiki HTTP-kõnesid.

Vaade

Vaated on üsna suured, kuna see peab kandma palju kohustusi, kuna olen kogu vaatekoodi UIViewController s-st hoolikalt eraldanud. Olen teinud järgmist:



Kui olete visanud UITableView segus on vaated nüüd palju suuremad kui UIViewController s, mis toob kaasa murettekitava 300+ koodirea ja palju segatud ülesandeid ChatView -s.

Kontroller

Kuna kogu mudeli käsitsemise loogika on liikunud jaotisse ChatModel Kogu vaatekood - mis võib siin peituda vähem optimaalsetes, eraldatud projektides - elab nüüd vaates, nii et UIViewController s on üsna õhukesed. Vaatekontroller ei tea täielikult, kuidas mudeli andmed välja näevad, kuidas neid tõmmatakse või kuidas neid peaks kuvama - need lihtsalt koordineerivad. Näidisprojektis ei lähe ükski UIViewController s üle 150 koodirea.

ViewController teeb siiski järgmisi asju:

See on endiselt palju, kuid see on enamasti kooskõlastamine, tagasihelistusplokkide töötlemine ja suunamine.

Kasu

Negatiivsed küljed

Probleemi määratlus

See töötab väga hästi seni, kuni rakendus järgib kasutaja toiminguid ja reageerib neile, nagu kujutaksite ette, et mõni selline rakendus nagu Adobe Photoshop või Microsoft Word töötab. Kasutaja teeb toimingu, kasutajaliidest värskendatakse, korratakse.

Kuid tänapäevased rakendused on ühendatud, sageli mitmel viisil. Näiteks suhtlete REST API kaudu, saate tõukemärguandeid ja mõnel juhul loote ühenduse ka WebSocketiga.

Sellega peab vaatajakontroller äkki tegelema rohkemate teabeallikatega ja alati, kui võetakse vastu väline teade, ilma et kasutaja seda käivitaks - nagu näiteks sõnumi vastuvõtmine WebSocket'i kaudu, peavad teabeallikad leidma tee tagasi paremale vaatekontrollerid. See vajab palju koodi, et kõik osad kokku kleepida, et täita põhimõtteliselt sama ülesanne.

Välised andmeallikad

Vaatame, mis juhtub, kui saame tõukesõnumi:

class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure('Chat for received message should always exist') } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

Peame vaatekontrollerite virna käsitsi läbi kaevama, et välja selgitada, kas on olemas mõni kontroller, mis peab pärast tõukemärguande saamist ennast värskendama. Sel juhul tahame värskendada ka ekraane, mis rakendavad UpdatedChatDelegate, mis antud juhul on ainult ChatsViewController. Teeme seda ka selleks, et teada saada, kas peaksime märguande pärssima, kuna vaatame juba Chat see oli mõeldud. Sel juhul edastame sõnumi lõpuks vaatekontrollerile. On üsna selge, et PushNotificationController peab oma töö tegemiseks rakendusest liiga palju teadma.

Kui ChatWebSocket edastaks sõnumeid ka rakenduse muudesse osadesse, selle asemel, et meil oleks üks-ühele suhe ChatViewController -ga, seisaksime seal silmitsi sama probleemiga.

On selge, et peame kirjutama üsna invasiivse koodi iga kord, kui lisame mõne muu välise allika. See kood on ka üsna habras, kuna see tugineb suuresti rakenduse struktuurile ja delegeerib andmete edastamise hierarhiasse üles töötamiseks.

Delegaadid

MVC muster lisab segule ka täiendava keerukuse, kui lisame muud vaate kontrollerid. Selle põhjuseks on asjaolu, et vaate kontrollerid kipuvad üksteisest teadma delegaatide, initsialisaatorite ja - klaviatuuride puhul - prepareForSegue andmete ja viidete edastamisel. Iga vaate kontroller haldab oma ühendusi mudeli või vahendavate kontrolleritega ning nad mõlemad saadavad ja saavad värskendusi.

Samuti edastavad vaated delegaatide kaudu vaate kontrolleritele tagasi. Kuigi see töötab, tähendab see, et andmete edastamiseks on meil vaja teha üsna palju samme, ja ma leian end alati tagasihelistamise ümber palju kontrollimas ja kontrollimas, kas delegaadid on tõesti seatud.

Ühte vaate kontrollerit on võimalik lõhkuda, muutes koodi teises, näiteks aegunud andmed ChatsListViewController -s sest ChatViewController ei helista updated(chat: Chat) enam. Eriti keerukamates olukordades on piin kõike hoida sünkroonis.

Vaate ja mudeli eraldamine

Eemaldades vaate kontrollerist kogu vaatega seotud koodi customView s ja teisaldades kogu mudeliga seotud koodi spetsiaalsetele kontrolleritele, on vaate kontroller üsna lahja ja eraldatud. Siiski on endiselt üks probleem: vaade, mida kuvada soovitakse, ja mudelis asuvad andmed on tühjad. Hea näide on ChatListView. Mida me tahame kuvada, on loetelu lahtritest, mis ütlevad meile, kellega me räägime, mis oli viimane sõnum, viimase sõnumi kuupäev ja mitu lugemata kirja on jäänud Chat

Lugemata sõnumiloendur vestlusekraanil.

Möödume siiski mudelist, mis ei tea, mida me näha tahame. Selle asemel on see lihtsalt Chat kontaktiga, mis sisaldab sõnumeid:

class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

Nüüd on võimalik kiiresti lisada mõni lisakood, mis annab meile viimase kirja ja sõnumite arvu, kuid stringide kuupäevade vormindamine on ülesanne, mis kuulub kindlalt vaate kihti:

var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

Lõpuks vormindame kuupäeva ChatItemTableViewCell -s kui seda kuvame:

func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? '' lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? '' show(unreadMessageCount: chat.unreadMessages) }

Isegi üsna lihtsa näite puhul on üsna selge, et vaate vajaduste ja mudeli vahel on pinge.

Staatiline sündmustepõhine MVVM, ka staatilisel sündmusel põhinev võtmine 'ViewModeli muster'

Staatiline MVVM töötab vaatemudelitega, kuid selle asemel, et luua nende kaudu kahesuunaline liiklus - umbes nagu meil oli varem MVC-ga vaate kontrolleri kaudu - loome muutumatud vaatemudelid, mis värskendavad kasutajaliidest iga kord, kui kasutajaliides peab vastusena sündmusele muutuma .

Sündmuse võib käivitada peaaegu iga koodi osa, kui see suudab esitada seotud andmed, mida sündmus nõuab enum Näiteks received(new: Message) sündmuse võib käivitada tõuketeatis, WebSocket või tavaline võrgukõne.

Vaatame seda skeemil:

MVVM-i juurutamise vooskeem.

Esmapilgul näib see olevat üsna keerulisem kui klassikaline MVC näide, kuna täpselt sama asja saavutamiseks on kaasatud palju rohkem klasse. Kuid lähemal vaatlusel pole ükski suhe enam kahesuunaline.

Veelgi olulisem on see, et iga kasutajaliidese värskenduse käivitab sündmus, nii et kõige juhtuva jaoks on rakenduse kaudu ainult üks marsruut. Kohe on selge, milliseid sündmusi võite oodata. Samuti on selge, kuhu peaksite vajadusel lisama uue või olemasolevatele sündmustele reageerides uue käitumise.

Pärast refaktoreerimist lõpetasin paljude uute tundidega, nagu ma eespool näitasin. Leiate minu staatilise MVVM-i versiooni rakendamise saidil GitHub . Kui aga võrdlen muudatusi cloc -ga tööriistaga selgub, et tegelikult pole nii palju lisakoodi üldse:

Muster Toimikud Tühi Kommentaar Kood
MVC 30 386 217 1807
MVVM 51 442 359 1981

Koodiridade arv on suurenenud vaid 9 protsenti. Veelgi olulisem on see, et nende failide keskmine suurus langes 60 koodirealt vaid 39-le.

Koodiliinide sektordiagrammid. Vaatekontrollerid: MVC 287 vs MVVM 154 ehk 47% vähem; Vaatamisi: MVC 523 vs MVVM 392 ehk 26% vähem.

Samuti on kõige suurem langus failides, mis on tavaliselt MVC-s kõige suuremad: vaated ja vaatekontrollerid. Vaated on vaid 74 protsenti nende algsest suurusest ja vaate kontrollerid on nüüd vaid 53 protsenti nende algsest suurusest.

Samuti tuleb märkida, et suur osa lisakoodist on raamatukogu kood, mis aitab visuaalipuus nuppude ja muude objektide külge plokke kinnitada, ilma et oleks vaja MVC klassikat @IBAction või delegeerige mustreid.

Uurime selle kujunduse erinevaid kihte ükshaaval.

Sündmus

Sündmus on alati enum, tavaliselt seotud väärtustega. Sageli kattuvad need teie mudeli ühe üksusega, kuid mitte tingimata. Sel juhul jaguneb rakendus kaheks põhisündmuseks enum s: ChatEvent ja MessageEvent. ChatEvent on kõigi vestlusobjektide värskenduste jaoks:

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

Teine käsitleb kõiki sõnumiga seotud sündmusi:

enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

Oluline on piirata oma *Event enum s mõistliku suurusega. Kui vajate 10 või enamat juhtumit, on see tavaliselt märk, mida proovite käsitleda rohkem kui ühe teema puhul.

Märkus: enum kontseptsioon on Swiftis äärmiselt võimas. Ma kipun kasutama enum s seotud väärtustega palju, kuna need võivad ära võtta palju ebaselgust, mis teil muidu valikuliste väärtustega oleks.

Kiire MVVM-i õpetus: sündmuste marsruuter

Sündmuste ruuter on iga rakenduses toimuva sündmuse lähtepunkt. Iga klass, mis suudab seostatud väärtuse pakkuda, saab sündmuse luua ja selle sündmuse ruuterisse saata. Nii et neid võivad käivitada mis tahes allikad, nt:

Sündmuse ruuter peaks teadma sündmuse allikast võimalikult vähe ja soovitavalt üldse mitte midagi. Ühelgi selle näidisrakenduse sündmusel pole ühtegi indikaatorit, kust need pärinevad, seega on mis tahes sõnumiallikaid väga lihtne segada. Näiteks käivitab WebSocket sama sündmuse - received(message: Message, contact: String) - uue tõuketeatisena.

Sündmused suunatakse (te arvasite juba) klassidesse, kes peavad neid sündmusi edasi töötlema. Tavaliselt on ainsad klassid, mida kutsutakse, mudelikiht (kui andmeid on vaja lisada, muuta või eemaldada) ja sündmuste käitleja. Ma arutan mõlemaid veel veidi edasi, kuid sündmuste ruuteri peamine omadus on anda kõigile sündmustele üks lihtne juurdepääsupunkt ja edastada töö teistele klassidele. Siin on ChatEventRouter näitena:

class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

Siin toimub üsna vähe: ainus asi, mida me teeme, on mudeli värskendamine ja sündmuse edastamine ChatEventHandler nii et kasutajaliidest värskendatakse.

Kiire MVVM-i õpetus: mudeli kontroller

See on täpselt sama klass, mida me kasutame MVC-s, kuna see töötas juba üsna hästi. See tähistab rakenduse olekut ja seda toetavad tavaliselt põhiandmed või kohalik salvestusraamatukogu.

Mudelikihid - kui need on MVC-s õigesti rakendatud - vajavad erinevate mustrite sobivaks muutmist väga harva. Suurim muudatus on see, et mudeli muutmine toimub vähemate klasside kaupa, muutes natuke selgemaks muutuste toimumise koha.

Selle mustri alternatiivse võtmise korral võite jälgida mudeli muudatusi ja veenduda, et neid käsitletakse. Sel juhul otsustasin lasta ainult *EventRouter ja *Endpoint klassid vahetavad mudelit, seega on selge vastutus selle eest, kus ja millal mudelit värskendatakse. Seevastu, kui me jälgiksime muutusi, peaksime kirjutama täiendava koodi, et levitada mudeleid mittemuutvaid sündmusi nagu vead ChatEventHandler kaudu, mis muudaks vähem ilmseks, kuidas sündmused rakenduse kaudu voolavad.

Kiire MVVM-i õpetus: sündmuste käitleja

Sündmuste käitleja on koht, kus vaated või vaadete kontrollerid saavad end kuulajatena registreerida (ja registreeruda), et saada värskendatud vaatemudeleid, mis on ehitatud alati, kui ChatEventRouter kutsub funktsiooni ChatEventHandler.

Näete, et see peegeldab ligikaudu kõiki vaate olekuid, mida me varem MVC-s kasutasime. Kui soovite muud tüüpi kasutajaliidese värskendusi - näiteks heli või Taptic-mootori käivitamist -, saate neid teha ka siit.

protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

See klass teeb midagi muud, kui veendub, et õige kuulaja saab õige vaatemudeli alati, kui mõni kindel sündmus on juhtunud. Uued kuulajad saavad vaatemudeli kohe pärast lisamist, kui see on vajalik nende esialgse oleku seadistamiseks. Veenduge, et lisate alati weak viide loendile hoidmistsüklite vältimiseks.

Kiire MVVM-i õpetus: vaadake mudelit

Siin on üks suurimaid erinevusi selle vahel, mida paljud MVVM-i mustrid teevad, võrreldes staatilise variandiga. Sel juhul on vaatemudel muutumatu, selle asemel et seadistada end püsiva kahesuunalise vahepealsena mudeli ja vaate vahele. Miks me seda teeksime? Peatume, et see hetk selgitada.

Kõigil võimalikel juhtudel hästi toimiva rakenduse loomise üks olulisemaid aspekte on veenduda, et rakenduse olek on õige. Kui kasutajaliides ei vasta mudelile või sellel on vananenud andmed, võib kõik meie tegevus kaasa tuua vigaste andmete salvestamise või rakenduse krahhi või ootamatu käitumise.

Selle mustri rakendamise üks eesmärke on see, et meil pole rakenduses olekut, kui see pole tingimata vajalik. Mis on täpselt riik? Osariik on põhimõtteliselt iga koht, kuhu me salvestame teatud tüüpi andmete esituse. Üks eriline olekutüüp on olek, milles teie kasutajaliides praegu on, mida loomulikult ei saa kasutajaliidese juhitud rakendusega ära hoida. Muud olekutüübid on kõik andmetega seotud. Kui meil on Chat s massiivi koopia, mis varundab meie UITableView vestlusloendi ekraanil on see näide duplikaatolekust. Traditsiooniline kahesuunalise vaatega mudel oleks veel üks näide meie kasutaja Chat s duplikaadist.

Läbides muutumatu vaatemudeli, mida värskendatakse iga mudeli muutmise korral, välistame seda tüüpi duplikaatide oleku, sest pärast seda, kui see kehtib kasutajaliidese kohta, seda enam ei kasutata. Siis on meil ainult ainsat tüüpi olekud, mida me vältida ei saa - kasutajaliides ja mudel - ning need on omavahel täiesti sünkroonis.

Nii et siinne vaatemudel erineb mõnest MVVM-i rakendusest. See toimib ainult muutumatute andmesalvestitena kõigi lippude, väärtuste, plokkide ja muude väärtuste jaoks, mida vaade mudeli oleku kajastamiseks nõuab, kuid vaade ei saa seda mingil viisil värskendada.

Seetõttu võib see olla lihtne muutumatu struct. Selle säilitamiseks struct võimalikult lihtsaks, instantsime selle vaatemudeli koostajaga. Vaatemudeli üks huvitav asi on see, et see saab käitumislippe nagu shouldShowBusy ja shouldShowError mis asendavad riiki enum varem vaates leitud mehhanism. Siin on andmed ChatItemTableViewCell kohta olime varem analüüsinud:

struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

Kuna vaate mudeli koostaja hoolitseb juba vaate täpsete väärtuste ja toimingute eest, on kõik andmed eelvormindatud. Uus on ka plokk, mis käivitatakse, kui üksus on puudutatud. Vaatame, kuidas selle saab vaate mudeli koostaja.

Kuva mudeli koostaja

Vaatemudeli koostaja saab luua vaatemudelite eksemplare, teisendades sisendi nagu Chat s või Message s vaatemudeliteks, mis on teatud vaate jaoks ideaalselt kohandatud. Üks olulisemaid asju, mis vaate mudeli koostajas juhtub, on selle määramine, mis vaate mudeli plokkides tegelikult toimub. Vaate mudeli koostaja poolt kinnitatud plokid peaksid olema äärmiselt lühikesed, kutsudes võimalikult kiiresti üles arhitektuuri teiste osade funktsioone. Sellistel plokkidel ei tohiks olla mingit äriloogikat.

class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? '' let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)

) } ?? '' let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

Nüüd toimub kogu eelvormindamine samas kohas ja käitumine otsustatakse ka siin. See on selles hierarhias üsna oluline klass ja võib olla huvitav näha, kuidas demorakenduse erinevad ehitajad on rakendatud, ja käsitleda keerulisemaid stsenaariume.

Kiire MVVM-i õpetus: kontrolleri kuvamine

Selle arhitektuuri vaatekontroller teeb väga vähe. See paneb paika ja lõhub kõik, mis on seotud tema seisukohaga. Parim on seda teha, sest see saab kõik elutsükli tagasihelistamised, mis on vajalikud kuulajate lisamiseks ja eemaldamiseks õigel ajal.

Mõnikord peab see värskendama kasutajaliidese elementi, mida juurvaade ei hõlma, näiteks pealkirja või nuppu navigeerimisribal. Sellepärast registreerin vaate kontrolleri tavaliselt ikkagi sündmuste ruuteri kuulajana, kui mul on vaate mudel, mis hõlmab kogu vaate kontrolleri kogu vaadet; Edastan vaate mudeli hiljem vaatele. Kuid on hea registreerida ka mis tahes UIView otse kuulajana, kui ekraanil on mõni muu värskendussagedusega osa, nt. teatud ettevõtte kohta käiva lehe otsas olev otseaktsia.

kohanduvate veebidisaini piltide parimad tavad

ChatsViewController Koodi on nüüd nii lühike, et võtab vähem kui leht. Järele jääb põhivaate tühistamine, nupu lisamine ja eemaldamine navigeerimisribalt, pealkirja määramine, kuulajaks lisamine ja ChatListListening protokoll:

class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = 'Chats' } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

Mujal pole midagi muud teha, kui ChatsViewController eemaldatakse miinimumini.

Kiire MVVM-i õpetus: vaade

Vaade muutumatul MVVM-i arhitektuuril võib endiselt olla üsna raske, kuna sellel on endiselt ülesannete loend, kuid mul õnnestus sellega MVC-arhitektuuriga võrreldes järgmised kohustused ära võtta:

Eriti viimasel punktil on üsna suur eelis. MVC-s, kui vaade või vaate kontroller vastutab kuvamiseks vajalike andmete teisendamise eest, teeb see seda alati peaniidil, kuna on väga raske eraldada kasutajaliidese tõelisi muudatusi, mis peavad sellel lõimal toimuma, asjadest, mis on pole kohustatud sellel jooksma. Ja kui põhilõigul töötab muud kui kasutajaliidese muutmata kood, võib see põhjustada vähem reageeriva rakenduse.

Selle MVVM-i mustri asemel on kõik alates puudutusest käivitatavast plokist kuni vaatemudeli ehitamise hetkeni ja edastatakse kuulajale - me saame seda kõike käitada eraldi lõimes ja sukelduda ainult kasutajaliidese värskenduste tegemise lõpp. Kui meie rakendus veedab vähem aega peaniidil, töötab see sujuvamalt.

Kui vaatemudel rakendab vaate jaoks uue oleku, lastakse sellel uue olekukihina ringi vajumise asemel aurustuda. Kõik, mis võib sündmuse käivitada, on lisatud vaate üksusele ja me ei suhtle tagasi vaatemudeliga.

Üks asi on oluline meeles pidada: te ei ole sunnitud vaatemudeli vaatekontrolleri kaudu vaatele kaardistama. Nagu varem mainitud, saab vaate osi hallata teiste vaatemudelite abil, eriti kui värskenduste määr erineb. Mõelge Google'i lehe muutmisele erinevate inimeste poolt, hoides vestluspaani kaastöötajatele avatud - pole eriti kasulik dokumenti värskendada, kui saabub vestlussõnum.

Tuntud näide on tüübist leidmine, kus otsingukasti värskendatakse täpsemate tulemustega, kui sisestame rohkem teksti. Nii rakendaksin automaatse täitmise rakenduses CreateAutocompleteView klass: kogu ekraani teenindab CreateViewModel kuid tekstikast kuulab AutocompleteContactViewModel selle asemel.

Teine näide on vormivalideerija kasutamine, mida saab kas ehitada „lokaalseks silmuseks” (väljadele veaolekute lisamine või eemaldamine ja vormi kehtivaks kuulutamine) või sündmus käivitades.

Staatilised muutumatud vaate mudelid tagavad parema eraldamise

Staatilise MVVM-i juurutamise abil oleme suutnud lõpuks kõik kihid täielikult eraldada, kuna vaatemudel ühendab nüüd mudeli ja vaate vahel. Samuti hõlbustasime sündmuste haldamist, mis ei olnud põhjustatud kasutaja tegevusest, ja eemaldasime palju sõltuvusi meie rakenduse erinevate osade vahel. Ainus asi, mida vaade kontroller teeb, on registreerida (ja registreerida ennast) sündmuste käitlejate juures kuulajate jaoks sündmuste jaoks, mida ta soovib saada.

Eelised:

Negatiivsed küljed:

Suurepärane on see, et see on puhas Swifti muster: see ei vaja kolmanda osapoole Swift MVVM-i raamistikku ega välista klassikalise MVC kasutamist, nii et saate hõlpsalt lisada oma rakenduse uusi funktsioone või refaktoreid täna ilma sunnitud kogu teie rakenduse ümber kirjutama.

Suurte vaateregulaatorite vastu võitlemiseks on ka teisi lähenemisviise, mis tagavad ka parema eraldatuse. Ma ei suutnud neid kõiki üksikasjalikult lisada, et neid võrrelda, kuid vaatame lühidalt mõnda alternatiivi:

Traditsiooniline MVVM asendab suurema osa vaate kontrolleri koodist vaatemudeliga, mis on lihtsalt tavaklass ja mida saab eraldi testida. Kuna see peab olema kahesuunaline sild vaate ja mudeli vahel, rakendab see sageli mingeid vaatluse vorme. Sellepärast näete seda sageli koos raamistikuga nagu RxSwift.

MVP ja VIPER tegelevad mudeli ja vaate vaheliste täiendavate abstraktsioonikihtidega traditsioonilisemal viisil, samal ajal kui Reactive muudab tõesti andmete ja sündmuste voogu teie rakenduses.

Reaktiivne programmeerimisstiil kogub viimasel ajal palju populaarsust ja on tegelikult üsna lähedal staatilisele MVVM-i lähenemisele sündmustega, nagu on selles artiklis selgitatud. Suurim erinevus on see, et see nõuab tavaliselt raamistikku ja suur osa teie koodist on spetsiaalselt sellele raamistikule suunatud.

MVP on muster, kus nii vaate kontrollerit kui ka vaadet peetakse vaate kihiks. Ettekandja teisendab mudeli ja edastab selle vaate kihile, samal ajal kui mina muudan andmed kõigepealt vaatemudeliks. Kuna vaadet saab protokollina abstraktseks muuta, on seda palju lihtsam testida.

VIPER võtab saatejuhi MVP-st, lisab äriloogika jaoks eraldi 'interaktori', nimetab mudeli kihti 'olemiks' ja tal on navigeerimise eesmärgil ruuter (ja akronüümi täitmiseks). Seda võib pidada MVP üksikasjalikumaks ja lahti seotud vormiks.


Nii et teil on see: staatiline sündmusest juhitud MVVM selgitas. Ootan huviga allpool toodud kommentaare!

Seotud: Kiire õpetus: sissejuhatus MVVM-i kujundusmustrisse

Põhitõdede mõistmine

Mis on MVVM-i kasutamine?

Vaate mudel on eraldi ja hõlpsasti testitav klass, mis võtab vaate kontrollerilt üle kogu loogika ja mudelilt vaatele koodi - ja sageli ka vaate mudeli vahelise sidumise.

Mis on iOS-i protokollid?

Protokollid (teistes keeltes nimetatakse neid sageli liidesteks) on funktsioonide ja muutujate kogum, mida saab rakendada mis tahes klass või struktuur. Kuna protokollid pole seotud kindla klassiga, on protokolli viite jaoks võimalik kasutada mis tahes klassi, kui see seda rakendab. See muudab selle palju paindlikumaks.

Milline on iOS-i delegeerimismuster?

Delegaat on nõrk viide teisele klassile, mis põhineb protokollil. Delegaate kasutatakse tavaliselt pärast ülesande täitmist teisele objektile „teatamiseks”, sidumata ennast kindlasse klassi ega teadmata selle kõiki üksikasju.

raspberry pi 3 koduserver

Mis vahe on MVC ja MVVM vahel?

IOS-is ei asenda MVVM MVC-d, see on täiendus. Vaatekontroller mängib endiselt rolli, kuid vaate mudel muutub vaate ja mudeli vahepealseks.

Mis on MVP iOS-is?

IOS-is on MVP (mudel-vaade-esitleja) muster, kus UIViews ja UIViewController on mõlemad vaatekihi osa. (Segane on see, et vaatekiht on arhitektuurne kontseptsioon, samas kui UIView on pärit UIKitilt ja seda nimetatakse tavaliselt ka vaateks.)