} viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
} viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
} viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
} viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
}

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
} viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
} viewModel.isFinished.bindAndFire { [unowned self] in if

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
{ self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title =

Ātra apmācība: Ievads MVVM dizaina modelī

Tātad jūs sākat jaunu iOS projektu, un jūs saņēmāt no dizainera visu nepieciešamo .pdf un .sketch dokumentus, un jums jau ir redzējums par to, kā veidosiet šo jauno lietotni.

Jūs sākat lietotāja interfeisa ekrānus no dizainera skicēm pārsūtīt uz ViewController .swift, .xib un .storyboard failus.

UITextField šeit, UITableView tur vēl pāris UILabels un šķipsnu UIButtons. IBOutlets un IBActions ir iekļauti arī. Viss labi, mēs joprojām esam UI zonā.



Tomēr ir pienācis laiks kaut ko darīt ar visiem šiem lietotāja saskarnes elementiem; UIButtons saņems pirkstu pieskārienus, UILabels un UITableViews būs vajadzīgs kāds, kurš viņiem pateiks, ko un kādā formātā parādīt.

Pēkšņi jums ir vairāk nekā 3000 koda rindu.

3000 Swift koda rindiņu

Jūs beidzāt ar daudz spageti kodu.

Pirmais solis, lai to atrisinātu, ir Model-View-Controller (MVC) dizaina modelis. Tomēr šim modelim ir savi jautājumi. Tur nāk Model-View-ViewModel (MVVM) dizaina modelis, kas ietaupa dienu.

Nodarbošanās ar spageti kodu

Īsā laikā jūsu sākums ViewController ir kļuvis pārāk gudrs un pārāk masīvs.

Tīkla kods, datu parsēšanas kods, datu korekcijas kods lietotāja saskarnes prezentācijai, lietotnes stāvokļa paziņojumi, lietotāja saskarnes stāvokļa izmaiņas. Šis kods ir ievietots viena faila if -oloģijā, kuru nevar atkārtoti izmantot un kas der tikai šim projektam.

Jūsu ViewController kods ir kļuvis par bēdīgi slaveno spageti kodu.

Kā tas notika?

Iespējamais iemesls ir kaut kas līdzīgs šim:

Jūs steidzāties, lai redzētu, kā aizmugures dati uzvedas UITableView iekšienē, tāpēc ievietojāt dažas tīkla koda rindiņas temp metode ViewController tikai lai to ienestu .json no tīkla. Pēc tam jums bija jāapstrādā dati .json iekšpusē, tāpēc jūs uzrakstījāt vēl vienu temp metode, kā to paveikt. Vai, vēl sliktāk, jūs to izdarījāt tajā pašā metodē.

ViewController turpināja pieaugt, kad parādījās lietotāja autorizācijas kods. Tad datu formāti sāka mainīties, lietotāja interfeiss attīstījās un bija nepieciešamas radikālas izmaiņas, un jūs vienkārši turpinājāt pievienot vēl if s jau tā masveida if -oloģijā.

Bet kā notiek UIViewController kas ir no rokas?

UIViewController ir loģiska vieta, kur sākt strādāt ar lietotāja saskarnes kodu. Tas attēlo fizisko ekrānu, kuru redzat, lietojot jebkuru lietotni savā iOS ierīcē. Pat Apple izmanto UIViewControllers savā galvenajā sistēmas lietotnē, kad pārslēdzas starp dažādām lietotnēm un animētajiem lietotāja interfeisiem.

Apple savu lietotāja interfeisa abstrakciju pamato UIViewController iekšpusē, jo tas ir iOS lietotāja saskarnes kodola pamatā un daļa no MVC dizaina raksts.

Saistīts: 10 izplatītākās iOS izstrādātāju kļūdas nezina, ka tās pieļauj

Jaunināšana uz MVC dizaina modeli

MVC dizaina modelis

MVC dizaina modelī Skats it kā nav aktīvs un parāda tikai sagatavotus datus pēc pieprasījuma.

Kontrolieris vajadzētu strādāt pie Modelis dati, lai tos sagatavotu Skati , kas pēc tam parāda šos datus.

Skats ir atbildīga arī par ESP paziņošanu Kontrolieris par jebkādām darbībām, piemēram, lietotāja pieskārieniem.

Kā minēts, UIViewController parasti ir sākumpunkts, lai izveidotu lietotāja saskarnes ekrānu. Ievērojiet, ka tā nosaukumā ir gan skats, gan kontrolieris. Tas nozīmē, ka tas “kontrolē skatu”. Tas nenozīmē, ka gan “kontroliera”, gan “skata” kodam ir jāiet iekšā.

Šis skata un kontroliera koda sajaukums bieži notiek, pārvietojoties IBOutlets mazo apakšskatījumu UIViewController iekšpusē, un manipulējiet ar šīm apakšskatām tieši no UIViewController. Tā vietā jums vajadzētu ietīt šo kodu pielāgotā UIView iekšpusē apakšklase.

Viegli redzēt, ka tas var novest pie tā, ka skata un kontroliera kodu ceļi tiek šķērsoti.

MVVM uz glābšanu

Tas ir, ja MVVM modelis ir noderīgs.

Tā kā UIViewController it kā ir Kontrolieris pēc MVC modeļa, un tas jau daudz dara ar Skati , mēs varam tos apvienot Skats mūsu jaunā modeļa - MVVM .

MVVM dizaina modelis

MVVM dizaina modelī Modelis ir tāds pats kā MVC modelī. Tas atspoguļo vienkāršus datus.

Skats ir attēlots ar UIView vai UIViewController objekti kopā ar to .xib un .storyboard faili, kuros jāparāda tikai sagatavotie dati. (Mēs nevēlamies, lai skatā būtu, piemēram, NSDateFormatter kods.)

Tikai vienkārša, formatēta virkne, kas nāk no ViewModel .

ViewModel slēpj visu asinhrono tīkla kodu, datu sagatavošanas kodu vizuālajai prezentācijai un kodu klausīšanos Modelis izmaiņas. Tas viss ir paslēpts aiz precīzi definēta API, kas modelēts tā, lai tas atbilstu tieši šai Skats .

Viena no MVVM izmantošanas priekšrocībām ir testēšana. Kopš ViewModel ir tīrs NSObject (vai, piemēram, struct), un tas nav savienots ar UIKit kodu, to varat vieglāk pārbaudīt vienības testos, neietekmējot lietotāja saskarnes kodu.

Tagad Skats (UIViewController / UIView) ir kļuvis daudz vienkāršāks ViewModel darbojas kā līme starp Modelis un Skats .

MVVM lietošana Swift

MVVM ātrā

Lai parādītu MVVM darbībā, varat lejupielādēt un pārbaudīt šai apmācībai izveidoto Xcode projekta piemēru šeit . Šis projekts izmanto Swift 3 un Xcode 8.1.

Ir divas projekta versijas: Starteris un Pabeigts .

The Pabeigts versija ir aizpildīta mini lietojumprogramma, kur Starteris ir tas pats projekts, bet bez ieviestajām metodēm un objektiem.

Pirmkārt, es iesaku jums lejupielādēt Starteris un izpildiet šo apmācību. Ja jums nepieciešama ātra projekta uzziņa vēlāk, lejupielādējiet Pabeigts projektu.

Mācību projekta ievads

Apmācības projekts ir basketbola programma, lai izsekotu spēlētāju darbības spēles laikā.

Basketbola aplikācija

To izmanto, lai ātri izsekotu lietotāju kustības un kopējo rezultātu pikapa spēlē.

Divas komandas spēlē, līdz tiek sasniegts rezultāts 15 (ar vismaz divu punktu starpību). Katrs spēlētājs var iegūt vienu punktu līdz diviem punktiem, un katrs spēlētājs var palīdzēt, atlecošā bumba un pārkāpums.

Projekta hierarhija izskatās šādi:

Projekta hierarhija

Modelis

Skats

ViewModel

Lejupielādētajā Xcode projektā jau ir vietas vietturi Skats objekti (UIView un UIViewController). Projektā ir arī daži pēc pasūtījuma izgatavoti objekti, kas izgatavoti, lai demonstrētu vienu no veidiem, kā nodrošināt datus ViewModel objekti (Services grupa).

Extensions grupa satur noderīgus lietotāja interfeisa koda paplašinājumus, kas neietilpst šīs apmācības darbības jomā un ir pašsaprotami.

Ja šajā brīdī palaidīsit lietotni, tajā tiks parādīts gatavs lietotāja interfeiss, taču nekas nenotiek, kad lietotājs nospiež pogas.

Tas ir tāpēc, ka esat izveidojis tikai skatus un IBActions nepievienojot tos lietotnes loģikai un neaizpildot lietotāja saskarnes elementus ar modeļa datiem (no Game objekta, kā mēs uzzināsim vēlāk).

Skata un modeļa savienošana ar ViewModel

MVVM dizaina shēmā View nevajadzētu zināt neko par modeli. Vienīgais, ko View zina, ir tas, kā strādāt ar ViewModel.

Vispirms pārbaudiet savu skatu.

In GameScoreboardEditorViewController.swift failu, fillUI metode šajā brīdī ir tukša. Šī ir vieta, kurā vēlaties aizpildīt lietotāja interfeisu ar datiem. Lai to panāktu, jums jāsniedz dati par ViewController. Jūs to darāt ar ViewModel objektu.

Vispirms izveidojiet objektu ViewModel, kas satur visus šim ViewController nepieciešamos datus.

Dodieties uz ViewModel Xcode projektu grupu, kas būs tukša, izveidojiet GameScoreboardEditorViewModel.swift failu un padariet to par protokolu.

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Šādu protokolu izmantošana saglabā lietu jauku un tīru; jums ir jādefinē tikai tie dati, kurus izmantosiet.

Pēc tam izveidojiet šī protokola ieviešanu.

Izveidojiet jaunu failu ar nosaukumu GameScoreboardEditorViewModelFromGame.swift un padariet šo objektu par NSObject apakšklasi.

Padariet to arī atbilstošu GameScoreboardEditorViewModel protokols:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

Ievērojiet, ka esat nodrošinājis visu nepieciešamo, lai ViewModel darbotos, izmantojot inicializētāju.

Jūs to norādījāt Game objekts, kas ir modelis zem šī ViewModel.

Ja palaidīsit lietotni tagad, tā joprojām nedarbosies, jo neesat savienojis šos ViewModel datus ar pašu View.

Tātad, atgriezieties pie GameScoreboardEditorViewController.swift failu un izveidojiet publisku īpašumu ar nosaukumu viewModel.

Izveidojiet to GameScoreboardEditorViewModel.

Novietojiet to tieši pirms viewDidLoad metodi GameScoreboardEditorViewController.swift iekšpusē.

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Pēc tam jums jāievieš fillUI metodi.

Ievērojiet, kā šī metode tiek izsaukta no divām vietām - viewModel īpašuma novērotājs (didSet) un viewDidLoad metodi. Tas ir tāpēc, ka mēs varam izveidot ViewController un piešķiriet tam ViewModel, pirms to pievienojat skatam (pirms tiek izsaukta viewDidLoad metode).

No otras puses, jūs varētu pievienot ViewController skatu citam skatam un izsaukt viewDidLoad, bet, ja viewModel tajā laikā nav iestatīts, nekas nenotiks.

Tāpēc vispirms jums jāpārbauda, ​​vai jūsu datiem ir iestatīts viss, lai aizpildītu lietotāja saskarni. Ir svarīgi aizsargāt kodu pret neparedzētu lietošanu.

Tātad, dodieties uz fillUI metodi un aizstājiet to ar šādu kodu:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

Tagad ieviesiet pauseButtonPress metode:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Viss, kas jums jādara tagad, ir iestatīts faktiskais viewModel īpašums šajā ViewController. Jūs to darāt “no ārpuses”.

Atvērt HomeViewController.swift failu un atcelt ViewModel komentāru; izveidot un iestatīt rindas showGameScoreboardEditorViewController metode:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Tagad palaidiet lietotni. Tam vajadzētu izskatīties apmēram šādi:

iOS lietotne

Vidējā skatā, kas ir atbildīgs par rezultātu, laiku un komandu nosaukumiem, vairs netiek rādītas saskarnes veidotājā iestatītās vērtības.

Tagad tas parāda vērtības no paša ViewModel objekta, kas datus iegūst no faktiskā modeļa objekta (Game objekts).

Izcili! Bet kā ar spēlētāja skatījumiem? Šīs pogas joprojām neko nedara.

Jūs zināt, ka jums ir seši skati spēlētāja kustību izsekošanai.

Jūs izveidojāt atsevišķu apakšskatījumu ar nosaukumu PlayerScoreboardMoveEditorView Šim nolūkam pagaidām nekas netiek darīts ar reālajiem datiem un tiek rādītas statiskās vērtības, kas tika iestatītas, izmantojot saskarnes veidotāju PlayerScoreboardMoveEditorView.xib failu.

Jums ir jāsniedz tam daži dati.

Jūs darīsit tāpat kā ar GameScoreboardEditorViewController un GameScoreboardEditorViewModel.

Xcode projektā atveriet ViewModel grupu un šeit definējiet jauno protokolu.

Izveidojiet jaunu failu ar nosaukumu PlayerScoreboardMoveEditorViewModel.swift un ievietojiet šādu kodu:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis ViewModel protokols tika izstrādāts tā, lai tas atbilstu jūsu PlayerScoreboardMoveEditorView, tāpat kā jūs to darījāt vecāku skatā GameScoreboardEditorViewController.

Jums ir jābūt vērtībām piecām dažādām kustībām, kuras lietotājs var veikt, un jums jāreaģē, kad lietotājs pieskaras vienai no darbības pogām. Jums nepieciešama arī String spēlētāja vārdam.

Kad esat to izdarījis, izveidojiet konkrētu klasi, kas ievieš šo protokolu tāpat kā vecāku skatā (GameScoreboardEditorViewController).

Pēc tam izveidojiet šī protokola ieviešanu: izveidojiet jaunu failu, nosauciet to PlayerScoreboardMoveEditorViewModelFromPlayer.swift un izveidojiet šo objektu par NSObject apakšklasi. Padariet to arī atbilstošu PlayerScoreboardMoveEditorViewModel protokols:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

Tagad jums ir nepieciešams objekts, kas izveidos šo gadījumu 'no ārpuses' un iestatīs to kā rekvizītu PlayerScoreboardMoveEditorView iekšpusē.

Atcerieties, kā HomeViewController bija atbildīgs par viewModel iestatīšanu īpašumu uz GameScoreboardEditorViewController?

Tādā pašā veidā GameScoreboardEditorViewController ir jūsu PlayerScoreboardMoveEditorView vecāku skats un tas GameScoreboardEditorViewController būs atbildīgs par PlayerScoreboardMoveEditorViewModel izveidi objektiem.

Jums jāpaplašina sava GameScoreboardEditorViewModel vispirms.

Atveriet GameScoreboardEditorViewMode l un pievienojiet šīs divas īpašības:

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Atjauniniet arī GameScoreboardEditorViewModelFromGame ar šīm divām īpašībām tieši virs initWithGame metode:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Pievienojiet šīs divas rindas iekšpusē initWithGame:

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Un, protams, pievienojiet trūkstošo playerViewModelsWithPlayers metode:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Lieliski!

Jūs esat atjauninājis savu ViewModel (GameScoreboardEditorViewModel) ar mājas un viesu spēlētāju klāstu. Jums joprojām jāaizpilda šie divi masīvi.

Jūs to izdarīsit tajā pašā vietā, kur izmantojāt šo viewModel lai aizpildītu lietotāja interfeisu.

Atvērt GameScoreboardEditorViewController un dodieties uz fillUI metodi. Pievienojiet šīs rindas metodes beigās:

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

Pašlaik jums ir kļūdas, jo neesat pievienojis faktisko viewModel īpašums PlayerScoreboardMoveEditorView iekšpusē.

Pievienojiet šo kodu virs init method inside the PlayerScoreboardMoveEditorView`.

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

Un ieviesiet fillUI metode:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Visbeidzot, palaidiet lietotni un noskaidrojiet, kā dati lietotāja saskarnes elementos ir faktiskie dati no Game objekts.

iOS lietotne

Šajā brīdī jums ir funkcionāla lietotne, kas izmanto MVVM dizaina modeli.

Tas labi paslēpj modeli no skata, un jūsu skats ir daudz vienkāršāks, nekā jūs pieradāt pie MVC.

Līdz šim esat izveidojis lietotni, kurā ir skats un tā ViewModel.

Šim skatam ir arī seši viena un tā paša apskata (atskaņotāja skata) gadījumi ar tā ViewModel.

Tomēr, kā pamanāt, datus lietotāja saskarnē var parādīt tikai vienu reizi (fillUI metodē), un šie dati ir statiski.

Ja jūsu dati skatījumos nemainīsies šī skata darbības laikā, jums ir labs un tīrs risinājums, kā šādā veidā izmantot MVVM.

ViewModel padarīšana par dinamisku

Tā kā jūsu dati mainīsies, jums jāveido ViewModel dinamisks.

Tas nozīmē, ka, mainoties modelim, ViewModel būtu jāmaina tā publiskā īpašuma vērtības; tas izplatītu izmaiņas atpakaļ skatā, kas ir tas, kas atjauninās lietotāja saskarni.

Ir daudz veidu, kā to izdarīt.

Kad mainās modelis, vispirms tiek saņemts paziņojums ViewModel.

Jums ir nepieciešams zināms mehānisms, lai izplatītu to, kas mainās līdz skatam.

Dažas no iespējām ietver RxSwift , kas ir diezgan liela bibliotēka un prasa zināmu laiku, lai pierastu.

ViewModel, iespējams, aktivizē NSNotification s par katru īpašuma vērtības maiņu, taču tas pievieno daudz kodu, kam nepieciešama papildu apstrāde, piemēram, paziņojumu abonēšana un abonēšanas atcelšana, kad skats tiek sadalīts.

Galvenās vērtības novērošana (KVO) ir vēl viena iespēja, taču lietotāji apstiprinās, ka tā API nav izdomāta.

Šajā apmācībā jūs izmantosiet Swift sugas un slēdzenes, kas ir labi aprakstītas Iesiešana, Generics, Swift un MVVM raksts .

Tagad atgriezīsimies pie lietotnes piemēra.

Dodieties uz ViewModel projektu grupu un izveidojiet jaunu Swift failu Dynamic.swift.

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Šo klasi izmantosiet ViewModels īpašumiem, kurus, domājams, mainīsit skata dzīves cikla laikā.

Vispirms sāciet ar PlayerScoreboardMoveEditorView un tā ViewModel, PlayerScoreboardMoveEditorViewModel.

Atvērt PlayerScoreboardMoveEditorViewModel un apskatīt tā īpašības.

Jo playerName nav paredzams, ka mainīsies, jūs varat atstāt to tādu, kāds tas ir.

Pārējās piecas īpašības (pieci pārvietošanās veidi) mainīsies, tāpēc jums kaut kas jādara šajā sakarā. Atrisinājums? Iepriekš minētie Dynamic klase, kuru tikko pievienojāt projektam.

Iekšpusē PlayerScoreboardMoveEditorViewModel noņemt definīcijas piecām virknēm, kas atspoguļo kustību skaitu, un aizstāt to ar šo:

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

Šādi jāizskatās ViewModel protokolam tagad:

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Šis Dynamic tips ļauj mainīt konkrētā rekvizīta vērtību un tajā pašā laikā paziņot izmaiņu klausītāja objektam, kas šajā gadījumā būs skats.

Tagad atjauniniet faktisko ViewModel ieviešanu PlayerScoreboardMoveEditorViewModelFromPlayer.

Nomainiet šo:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

ar sekojošo:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

Piezīme. Ir pareizi deklarēt šīs īpašības kā konstantes ar let jo jūs nemainīsit faktisko īpašumu. Jūs mainīsit value īpašums Dynamic objekts.

Tagad ir izveidotas kļūdas, jo neesat inicializējis savu Dynamic objektiem.

Iekšējā PlayerScoreboardMoveEditorViewModelFromPlayer init metodē pārvietošanas īpašību inicializāciju aizstājiet ar šo:

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

Iekšpusē PlayerScoreboardMoveEditorViewModelFromPlayer dodieties uz makeMove metodi un aizstājiet to ar šādu kodu:

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

Kā redzat, esat izveidojis Dynamic gadījumus klasi un piešķīra tai String vērtības. Kad jums jāatjaunina dati, nemainiet Dynamic pats īpašums; drīzāk atjauniniet to value īpašums.

Lieliski! PlayerScoreboardMoveEditorViewModel tagad ir dinamisks.

Izmantosim to un pārejiet pie skata, kas patiesībā uzklausīs šīs izmaiņas.

Atvērt PlayerScoreboardMoveEditorView un tā fillUI metode (šajā brīdī jums vajadzētu redzēt kļūdas šajā metodē, jo jūs mēģināt piešķirt String vērtību objekta tipam Dynamic)

Nomainiet “kļūdainās” rindas:

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

ar sekojošo:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Pēc tam ieviesiet piecas metodes, kas atspoguļo pārvietošanās darbības ( Pogas darbība sadaļā):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Palaidiet lietotni un noklikšķiniet uz dažām pārvietošanās pogām. Noklikšķinot uz darbības pogas, redzēsiet, kā atskaņotāja skatos mainās skaitītāju vērtības.

iOS lietotne

Jūs esat pabeidzis ar PlayerScoreboardMoveEditorView un PlayerScoreboardMoveEditorViewModel.

Tas bija vienkārši.

Tagad tas pats jādara ar galveno skatu (GameScoreboardEditorViewController).

Vispirms atveriet GameScoreboardEditorViewModel un redzēt, kuras vērtības skata dzīves cikla laikā ir paredzētas.

Aizstāt time, score, isFinished, isPaused definīcijas ar Dynamic versijas:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Dodieties uz ViewModel ieviešanu (GameScoreboardEditorViewModelFromGame) un dariet to pašu ar protokolā deklarētajām īpašībām.

Nomainiet šo:

var time: String var score: String var isFinished: Bool var isPaused: Bool

ar sekojošo:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

Tagad jūs saņemsiet dažas kļūdas, jo jūs mainījāt ViewModel veidu no String un Bool uz Dynamic un Dynamic.

Labosim to.

Novērst togglePause metodi, aizstājot to ar šādu:

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Ievērojiet, kā vienīgās izmaiņas ir tas, ka jūs vairs nenosaka īpašuma vērtību tieši īpašumā. Tā vietā jūs to iestatāt objekta value īpašums.

Tagad izlabojiet initWithGame metodi, aizstājot šo:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

ar sekojošo:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Jums vajadzētu saprast tagad.

Jūs iesaiņojat primitīvās vērtības, piemēram, String, Int un Bool, ar Dynamic šo objektu versijas, kas nodrošina vieglu iesiešanas mehānismu.

Jums ir jāizlabo vēl viena kļūda.

Vietā startTimer metodi, aizstājiet kļūdas rindu ar:

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Jūs esat jauninājis savu ViewModel, lai tas būtu dinamisks, tāpat kā jūs to darījāt ar atskaņotāja ViewModel. Bet jums joprojām ir jāatjaunina skats (GameScoreboardEditorViewController).

Nomainiet visu fillUI metodi ar šo:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība
? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

Vienīgā atšķirība ir tā, ka jūs mainījāt četras dinamiskās īpašības un katram no tiem pievienojāt izmaiņu klausītājus.

Šajā brīdī, ja palaižat savu lietotni, pārslēdzot Sākt / Pauze poga sāks un apturēs spēles taimeri. Tas tiek izmantots spēles pārtraukumiem.

Jūs gandrīz esat pabeidzis, izņemot to, ka rezultāts lietotāja saskarnē nemainās, nospiežot vienu no punktu pogām (1 un 2 punkti).

Tas ir tāpēc, ka jūs patiesībā neesat veicinājis rādītāja izmaiņas pamatā esošajā Game modeļa objektu līdz ViewModel.

ir mana llc s vai c corp

Tātad, atveriet Game modeļa objekts nelielai pārbaudei. Pārbaudiet tā updateScore metodi.

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Šī metode veic divas svarīgas lietas.

Pirmkārt, tas nosaka isFinished īpašums uz true ja spēle ir pabeigta, pamatojoties uz abu komandu rezultātiem.

Pēc tam tā publicē paziņojumu, ka rezultāts ir mainījies. Šo paziņojumu noklausīsities GameScoreboardEditorViewModelFromGame un atjauniniet dinamisko punktu vērtību paziņojumu apstrādes metodē.

Pievienojiet šo rindiņu initWithGame apakšā metodi (neaizmirstiet super.init() zvanu, lai izvairītos no kļūdām):

super.init() subscribeToNotifications()

Zemāk initWithGame metodi pievienojiet deinit metodi, jo vēlaties veikt tīrīšanu pareizi un izvairīties no avārijām, ko izraisa NotificationCenter.

deinit { unsubscribeFromNotifications() }

Visbeidzot, pievienojiet šo metožu ieviešanu. Pievienojiet šo sadaļu tieši zem deinit metode:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Tagad palaidiet lietotni un noklikšķiniet uz atskaņotāja skatiem, lai mainītu rezultātus. Tā kā jūs jau esat izveidojis savienojumu ar dinamisko score un isFinished ViewModel ar skatu visam vajadzētu darboties, mainot skalas vērtību ViewModel iekšpusē.

Kā vēl uzlabot lietotni

Lai gan vienmēr ir iespējas uzlabot, šī apmācība neietilpst.

Piemēram, mēs nepārtraucam laiku automātiski, kad spēle ir beigusies (kad viena no komandām sasniedz 15 punktus), mēs vienkārši slēpjam spēlētāju skatus.

Jūs varat spēlēt ar lietotni, ja vēlaties, un jauniniet to, lai būtu redzams “spēles veidotājs”, kas izveidotu spēli, piešķirtu komandu nosaukumus, piešķirtu spēlētāju vārdus un izveidotu Game objekts, ko varētu izmantot GameScoreboardEditorViewController

Mēs varam izveidot vēl vienu “spēļu saraksta” skatu, kurā tiek izmantots UITableView lai tabulas šūnā parādītu vairākas notiekošās spēles ar nelielu informāciju. Šūnas atlasē mēs varam parādīt GameScoreboardEditorViewController ar izvēlēto Game.

GameLibrary jau ir ieviests. Vienkārši atcerieties nodot šo bibliotēkas atsauci ViewModel objektiem to inicializatorā. Piemēram, “spēles veidotāja” ViewModel jābūt ar GameLibrary gadījumu iziet cauri inicializatoram, lai tas varētu ievietot izveidoto Game objektu bibliotēkā. “Spēļu saraksta” ViewModel būtu nepieciešama arī šī atsauce, lai no bibliotēkas iegūtu visas spēles, kas būs nepieciešamas UITableView.

Ideja ir slēpt visus netīros (ne UI) darbus ViewModel iekšienē un UI (View) rīkoties tikai ar sagatavotiem prezentācijas datiem.

Ko tagad?

Pēc tam, kad esat pieradis pie MVVM, varat to vēl vairāk uzlabot, izmantojot Uncle Bob's Clean Architecture noteikumi .

Papildu labs lasījums ir trīs daļu apmācība par Android arhitektūru:

Piemēri ir rakstīti Java valodā (operētājsistēmai Android), un, ja jūs esat iepazinies ar Java (kas ir daudz tuvāk Swift, tad Objective-C ir Java), jūs iegūsiet idejas, kā tālāk pārveidot kodu ViewModel objektos, lai ka viņi neimportē nevienu iOS moduli (UIKit vai CoreLocation piem.).

Šos iOS moduļus var paslēpt aiz tīra NSObjects, kas ir noderīgs koda atkārtotai lietošanai.

MVVM ir laba izvēle lielākajai daļai iOS lietotnes, un, cerams, jūs to izmēģināsiet nākamajā projektā. Vai arī izmēģiniet to pašreizējā projektā, kad veidojat UIViewController.

Saistīts: Darbs ar statiskiem modeļiem: ātra MVVM apmācība