Swift Eğitimi: MVVM Tasarım Modeline Giriş

Yayınlanan: 2022-03-11

Yeni bir iOS projesine başlıyorsunuz, tasarımcıdan gerekli tüm .pdf ve .sketch belgelerini aldınız ve bu yeni uygulamayı nasıl oluşturacağınıza dair bir vizyonunuz var.

Kullanıcı arayüzü ekranlarını tasarımcının eskizlerinden ViewController .swift , .xib ve .storyboard dosyalarınıza aktarmaya başlarsınız.

UITextField burada, UITableView orada, birkaç UILabels ve bir tutam UIButtons . IBOutlets ve IBActions da dahildir. Her şey yolunda, hala UI bölgesindeyiz.

Ancak, tüm bu UI öğeleriyle bir şeyler yapmanın zamanı geldi; UIButtons parmak dokunuşları alacak, UILabels ve UITableViews , onlara neyi ve hangi formatta göstereceklerini söyleyecek birine ihtiyaç duyacaktır.

Aniden, 3.000'den fazla kod satırınız var.

3.000 satır Swift kodu

Bir sürü spagetti koduyla bitirdiniz.

Bunu çözmenin ilk adımı, Model-View-Controller (MVC) tasarım desenini uygulamaktır. Ancak, bu modelin kendi sorunları vardır. Günü kurtaran Model-View-ViewModel (MVVM) tasarım deseni geliyor.

Spagetti Koduyla Başa Çıkmak

Hiçbir zaman, ViewController başlatmanız çok akıllı ve çok büyük hale geldi.

Ağ kodu, veri ayrıştırma kodu, UI sunumu için veri ayarlama kodu, uygulama durumu bildirimleri, UI durumu değişiklikleri. Tüm bu kod, yeniden kullanılamayan ve yalnızca bu projeye uyan tek bir dosyanın if -ology içine hapsedildi.

ViewController kodunuz kötü şöhretli spagetti kodu haline geldi.

Bu nasıl oldu?

Muhtemel sebep şöyle bir şeydir:

UITableView içinde arka uç verilerinin nasıl davrandığını görmek için aceleniz vardı, bu yüzden sadece o .json ağdan almak için ViewController geçici yöntemine birkaç satır ağ kodu koydunuz. Ardından, bu .json içindeki verileri işlemeniz gerekiyordu, bu nedenle bunu başarmak için başka bir geçici yöntem yazdınız. Ya da daha da kötüsü, bunu aynı yöntemle yaptınız.

Kullanıcı yetkilendirme kodu geldiğinde ViewController büyümeye devam etti. Sonra veri biçimleri değişmeye başladı, kullanıcı arayüzü gelişti ve bazı radikal değişikliklere ihtiyaç duydu ve siz zaten devasa bir if -ology'ye daha fazla if s eklemeye devam ettiniz.

Ancak, nasıl oluyor da UIViewController çıktı?

UIViewController , UI kodunuz üzerinde çalışmaya başlamak için mantıklı bir yerdir. iOS cihazınızla herhangi bir uygulamayı kullanırken gördüğünüz fiziksel ekranı temsil eder. Apple bile, farklı uygulamalar ve animasyonlu kullanıcı arayüzleri arasında geçiş yaparken ana sistem uygulamasında UIViewControllers kullanır.

Apple, UI soyutlamasını, iOS UI kodunun merkezinde ve MVC tasarım modelinin bir parçası olduğu için UIViewController içinde temel alır.

İlgili: iOS Geliştiricilerinin Yaptıklarını Bilmedikleri En Yaygın 10 Hata

MVC Tasarım Modeline Yükseltme

MVC Tasarım Modeli

MVC tasarım modelinde, Görünümün etkin olmaması ve yalnızca talep üzerine hazırlanmış verileri göstermesi gerekir.

Denetleyici , daha sonra bu verileri görüntüleyen Views için hazırlamak için Model verileri üzerinde çalışmalıdır.

Görünüm ayrıca, kullanıcı dokunuşları gibi herhangi bir eylem hakkında Denetleyiciyi bilgilendirmekten de sorumludur.

Belirtildiği gibi, UIViewController genellikle bir UI ekranı oluşturmanın başlangıç ​​noktasıdır. Adında hem "görünüm" hem de "kontrolör" içerdiğine dikkat edin. Bu, "görünümü kontrol ettiği" anlamına gelir. Bu, hem "kontrolör" hem de "görünüm" kodunun içeri girmesi gerektiği anlamına gelmez.

Bu görünüm ve denetleyici kodu karışımı, genellikle küçük alt görünümlerin IBOutlets UIViewController içinde hareket ettirdiğinizde ve bu alt görünümler üzerinde doğrudan UIViewController işlem yaptığınızda gerçekleşir. Bunun yerine, bu kodu özel bir UIView alt sınıfının içine sarmanız gerekirdi.

Bunun, Görünüm ve Denetleyici kod yollarının kesişmesine yol açabileceğini görmek kolaydır.

Kurtarmaya MVVM

MVVM modelinin kullanışlı olduğu yer burasıdır.

UIViewController MVC modelinde bir Controller olması gerektiği ve Views ile zaten çok şey yaptığı için, onları yeni modelimizin View - MVVM ile birleştirebiliriz.

MVVM Tasarım Modeli

MVVM tasarım modelinde Model , MVC modelindeki ile aynıdır. Basit verileri temsil eder.

Görünüm , yalnızca hazırlanmış verileri göstermesi gereken .xib ve .storyboard dosyalarıyla birlikte UIView veya UIViewController nesneleri tarafından temsil edilir. (Örneğin, Görünüm içinde NSDateFormatter koduna sahip olmak istemiyoruz.)

Yalnızca ViewModel'den gelen basit, biçimlendirilmiş bir dize.

ViewModel , tüm eşzamansız ağ oluşturma kodunu, görsel sunum için veri hazırlama kodunu ve Model değişiklikleri için kod dinlemeyi gizler. Bunların tümü, bu belirli Görünüme uyacak şekilde modellenmiş iyi tanımlanmış bir API'nin arkasına gizlenmiştir.

MVVM kullanmanın faydalarından biri test etmektir. ViewModel saf NSObject (veya örneğin struct ) olduğundan ve UIKit koduyla birleştirilmediğinden, UI kodunu etkilemeden birim testlerinizde daha kolay test edebilirsiniz.

Şimdi, View ( UIViewController / UIView ) çok daha basit hale gelirken, ViewModel Model ve View arasında yapıştırıcı görevi görür.

Swift'de MVVM Uygulamak

Swift'de MVVM

Size MVVM'yi çalışırken göstermek için, bu eğitim için oluşturulan örnek Xcode projesini buradan indirebilir ve inceleyebilirsiniz. Bu proje Swift 3 ve Xcode 8.1 kullanıyor.

Projenin iki versiyonu vardır: Başlangıç ​​ve Bitmiş.

Bitmiş sürüm, Starter'ın aynı proje olduğu ancak uygulanan yöntemler ve nesneler olmadan tamamlanmış bir mini uygulamadır.

Öncelikle Starter projesini indirmenizi ve bu öğreticiyi takip etmenizi öneririm. Daha sonra proje için hızlı bir referansa ihtiyacınız varsa, Bitti projesini indirin.

Eğitim Projesi Tanıtımı

Eğitim projesi, oyun sırasında oyuncu hareketlerinin takibi için bir basketbol uygulamasıdır.

Basketbol uygulaması

Bir toplama oyununda kullanıcı hareketlerinin ve genel puanın hızlı takibi için kullanılır.

İki takım, 15 puana (en az iki puanlık farkla) ulaşılana kadar oynar. Her oyuncu bir ila iki puan alabilir ve her oyuncu asist, ribaund ve faul yapabilir.

Proje hiyerarşisi şöyle görünür:

proje hiyerarşisi

modeli

  • Game.swift
    • Oyun mantığını içerir, toplam puanı izler, her oyuncunun hareketlerini izler.
  • Team.swift
    • Takım adını ve oyuncu listesini içerir (her takımda üç oyuncu).
  • Player.swift
    • Adı olan tek bir oyuncu.

Görünüm

  • HomeViewController.swift
    • GameScoreboardEditorViewController sunan kök görünüm denetleyicisi
  • GameScoreboardEditorViewController.swift
    • Main.storyboard Arayüz Oluşturucu görünümüyle desteklenir.
    • Bu eğitim için ilgi ekranı.
  • PlayerScoreboardMoveEditorView.swift
    • PlayerScoreboardMoveEditorView.xib Arayüz Oluşturucu görünümüyle desteklenir
    • Yukarıdaki görünümün alt görünümü, ayrıca MVVM tasarım desenini kullanır.

GörünümModeli

  • ViewModel grubu boş, bu eğitimde oluşturacağınız şey bu.

İndirilen Xcode projesi, View nesneleri ( UIView ve UIViewController ) için zaten yer tutucular içeriyor. Proje ayrıca, ViewModel nesnelerine ( Services grubu) veri sağlama yollarından birini göstermek için yapılmış bazı özel yapım nesneleri de içerir.

Extensions grubu, kullanıcı arabirimi kodu için bu öğreticinin kapsamında olmayan ve kendi kendini açıklayan yararlı uzantılar içerir.

Uygulamayı bu noktada çalıştırırsanız, bitmiş kullanıcı arayüzünü gösterir, ancak kullanıcı düğmelere bastığında hiçbir şey olmaz.

Bunun nedeni, görünümleri ve IBActions uygulama mantığına bağlamadan ve UI öğelerini modeldeki verilerle (daha sonra öğreneceğimiz gibi Game nesnesinden) doldurmadan oluşturmuş olmanızdır.

Görünüm ve Modeli ViewModel ile Bağlama

MVVM tasarım modelinde View, Model hakkında hiçbir şey bilmemelidir. View'in bildiği tek şey, bir ViewModel ile nasıl çalışılacağıdır.

Görünümünüzü inceleyerek başlayın.

GameScoreboardEditorViewController.swift dosyasında bu noktada fillUI yöntemi boştur. Bu, kullanıcı arayüzünü verilerle doldurmak istediğiniz yerdir. Bunu başarmak için ViewController için veri sağlamanız gerekir. Bunu bir ViewModel nesnesiyle yaparsınız.

İlk olarak, bu ViewController için gerekli tüm verileri içeren bir ViewModel nesnesi oluşturun.

Boş olacak olan ViewModel Xcode proje grubuna gidin, bir GameScoreboardEditorViewModel.swift dosyası oluşturun ve bunu bir protokol yapın.

 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(); }

Bunun gibi protokolleri kullanmak her şeyi güzel ve temiz tutar; sadece kullanacağınız verileri tanımlamanız gerekir.

Ardından, bu protokol için bir uygulama oluşturun.

GameScoreboardEditorViewModelFromGame.swift adlı yeni bir dosya oluşturun ve bu nesneyi NSObject bir alt sınıfı yapın.

Ayrıca GameScoreboardEditorViewModel protokolüne uygun hale getirin:

 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" <- because we want only 1 digit let totalSeconds: Int = totalMillis / 1000 let seconds: Int = totalSeconds % 60 let minutes: Int = (totalSeconds / 60) return String(format: "%02d:%02d.%d", minutes, seconds, millis) } fileprivate static func timeRemainingPretty(for game: Game) -> String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: "\(game.homeTeamScore) - \(game.awayTeamScore)") } }

ViewModel'in başlatıcı aracılığıyla çalışması için gereken her şeyi sağladığınıza dikkat edin.

Bu ViewModel'in altındaki Model olan Game nesnesini sağladınız.

Uygulamayı şimdi çalıştırırsanız, bu ViewModel verilerini Görünümün kendisine bağlamadığınız için yine de çalışmayacaktır.

Bu nedenle, GameScoreboardEditorViewController.swift dosyasına geri dönün ve viewModel adında bir genel özellik oluşturun.

GameScoreboardEditorViewModel türünden yapın.

GameScoreboardEditorViewController.swift içindeki viewDidLoad yönteminden hemen önce yerleştirin.

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

Ardından, fillUI yöntemini uygulamanız gerekir.

Bu yöntemin iki yerden nasıl çağrıldığına dikkat edin, viewModel özellik gözlemcisi ( didSet ) ve viewDidLoad yöntemi. Bunun nedeni, bir ViewController oluşturabilmemiz ve bir görünüme eklemeden önce ona bir ViewModel atayabilmemizdir ( viewDidLoad yöntemi çağrılmadan önce).

Öte yandan, ViewController'ın görünümünü başka bir görünüme ekleyebilir ve viewDidLoad öğesini çağırabilirsiniz, ancak o viewModel ayarlanmamışsa hiçbir şey olmaz.

Bu nedenle öncelikle, verilerinizin kullanıcı arayüzünü doldurması için her şeyin ayarlanıp ayarlanmadığını kontrol etmeniz gerekir. Kodunuzu beklenmedik kullanıma karşı korumak önemlidir.

Bu nedenle, fillUI yöntemine gidin ve aşağıdaki kodla değiştirin:

 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) }

Şimdi, pauseButtonPress yöntemini uygulayın:

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

Şimdi tek yapmanız gereken, bu ViewController üzerinde gerçek viewModel özelliğini ayarlamaktır. Bunu “dışarıdan” yaparsınız.

HomeViewController.swift dosyasını açın ve ViewModel'in yorumunu kaldırın; showGameScoreboardEditorViewController yönteminde satırlar oluşturun ve ayarlayın:

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

Şimdi uygulamayı çalıştırın. Bunun gibi bir şeye benzemeli:

iOS Uygulaması

Skor, süre ve takım adlarından sorumlu olan orta görünüm artık Arayüz Oluşturucu'da ayarlanan değerleri göstermiyor.

Şimdi, verilerini gerçek Model nesnesinden ( Game nesnesi) alan ViewModel nesnesinin kendisinden gelen değerleri gösteriyor.

Harika! Peki ya oyuncu görüşleri? Bu düğmeler hala hiçbir şey yapmıyor.

Oyuncu-hareket takibi için altı görüşünüz olduğunu biliyorsunuz.

Bunun için PlayerScoreboardMoveEditorView adlı ayrı bir alt görünüm oluşturdunuz, bu şu an için gerçek verilerle hiçbir şey yapmıyor ve PlayerScoreboardMoveEditorView.xib dosyasında Arayüz Oluşturucu aracılığıyla ayarlanan statik değerleri gösteriyor.

Ona biraz veri vermeniz gerekiyor.

Bunu GameScoreboardEditorViewController ve GameScoreboardEditorViewModel ile yaptığınız gibi yapacaksınız.

Xcode projesinde ViewModel grubunu açın ve yeni protokolü burada tanımlayın.

PlayerScoreboardMoveEditorViewModel.swift adlı yeni bir dosya oluşturun ve içine aşağıdaki kodu yerleştirin:

 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() }

Bu ViewModel protokolü, GameScoreboardEditorViewController ana görünümünde yaptığınız gibi PlayerScoreboardMoveEditorView 'ınıza uyacak şekilde tasarlanmıştır.

Bir kullanıcının yapabileceği beş farklı hamle için değerlere sahip olmanız ve kullanıcı işlem düğmelerinden birine dokunduğunda tepki vermeniz gerekir. Ayrıca oyuncu adı için bir String ihtiyacınız var.

Bunu yaptıktan sonra, tıpkı ana görünümde yaptığınız gibi ( GameScoreboardEditorViewController ) bu protokolü uygulayan somut bir sınıf oluşturun.

Ardından, bu protokolün bir uygulamasını oluşturun: Yeni bir dosya oluşturun, onu PlayerScoreboardMoveEditorViewModelFromPlayer.swift olarak adlandırın ve bu nesneyi NSObject öğesinin bir alt sınıfı yapın. Ayrıca, PlayerScoreboardMoveEditorViewModel protokolüne uygun hale getirin:

 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))" } }

Şimdi, bu örneği "dışarıdan" oluşturacak ve PlayerScoreboardMoveEditorView içinde bir özellik olarak ayarlayacak bir nesneye ihtiyacınız var.

GameScoreboardEditorViewController viewModel özelliğini ayarlamaktan HomeViewController nasıl sorumlu olduğunu hatırlıyor musunuz?

Aynı şekilde, GameScoreboardEditorViewController , PlayerScoreboardMoveEditorView bir üst görünümüdür ve GameScoreboardEditorViewController , PlayerScoreboardMoveEditorViewModel nesnelerinin oluşturulmasından sorumlu olacaktır.

Önce GameScoreboardEditorViewModel genişletmeniz gerekir.

GameScoreboardEditorViewMode l'yi açın ve şu iki özelliği ekleyin:

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

Ayrıca GameScoreboardEditorViewModelFromGame initWithGame yönteminin hemen üzerindeki şu iki özellikle güncelleyin:

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

initWithGame içine şu iki satırı ekleyin:

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

Ve elbette, eksik playerViewModelsWithPlayers yöntemini ekleyin:

 // 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 }

Harika!

ViewModel'inizi ( GameScoreboardEditorViewModel ) ev sahibi ve deplasman oyuncuları dizisiyle güncellediniz. Hala bu iki diziyi doldurmanız gerekiyor.

Bunu, kullanıcı arayüzünü doldurmak için bu viewModel kullandığınız yerde yapacaksınız.

GameScoreboardEditorViewController açın ve fillUI yöntemine gidin. Yöntemin sonuna şu satırları ekleyin:

 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]

Şu anda, PlayerScoreboardMoveEditorView içine gerçek viewModel özelliğini eklemediğiniz için derleme hatalarınız var.

PlayerScoreboardMoveEditorView` init method inside the üstüne aşağıdaki kodu ekleyin.

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

Ve fillUI yöntemini uygulayın:

 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 }

Son olarak, uygulamayı çalıştırın ve UI öğelerindeki verilerin Game nesnesinden gelen gerçek veriler olduğunu görün.

iOS Uygulaması

Bu noktada, MVVM tasarım modelini kullanan işlevsel bir uygulamanız var.

Modeli Görünümden güzel bir şekilde gizler ve Görünümünüz MVC ile alıştığınızdan çok daha basittir.

Bu noktaya kadar, Görünümü ve onun ViewModel'ini içeren bir uygulama oluşturdunuz.

Bu Görünüm ayrıca ViewModel ile aynı alt görünümün (oyuncu görünümü) altı örneğine sahiptir.

Ancak fark edebileceğiniz gibi, kullanıcı arayüzündeki verileri yalnızca bir kez görüntüleyebilirsiniz ( fillUI yönteminde) ve bu veriler statiktir.

Görünümlerdeki verileriniz bu görünümün ömrü boyunca değişmeyecekse, MVVM'yi bu şekilde kullanmak için iyi ve temiz bir çözümünüz var.

ViewModel'i Dinamik Yapmak

Verileriniz değişeceği için ViewModel'inizi dinamik hale getirmeniz gerekiyor.

Bunun anlamı, Model değiştiğinde ViewModel'in genel özellik değerlerini değiştirmesi gerektiğidir; değişikliği, kullanıcı arayüzünü güncelleyecek olan görünüme geri yayar.

Bunu yapmanın birçok yolu var.

Model değiştiğinde önce ViewModel bilgilendirilir.

Görünüme kadar değişenleri yaymak için bir mekanizmaya ihtiyacınız var.

Seçeneklerden bazıları, oldukça büyük bir kütüphane olan ve alışması biraz zaman alan RxSwift'i içerir.

ViewModel, her özellik değeri değişikliğinde NSNotification s tetikliyor olabilir, ancak bu, bildirimlere abone olmak ve görünümün yeri ayrıldığında abonelikten çıkmak gibi ek işleme gerektiren birçok kod ekler.

Anahtar Değer Gözlemleme (KVO) başka bir seçenektir, ancak kullanıcılar API'sinin süslü olmadığını onaylayacaktır.

Bu öğreticide, Bindings, Generics, Swift ve MVVM makalesinde güzel bir şekilde açıklanan Swift jeneriklerini ve kapanışlarını kullanacaksınız.

Şimdi örnek uygulamaya dönelim.

ViewModel proje grubuna gidin ve yeni bir Swift dosyası, Dynamic.swift .

 class Dynamic<T> { 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 } }

Bu sınıfı, ViewModels'inizdeki View yaşam döngüsü sırasında değiştirmeyi umduğunuz özellikler için kullanacaksınız.

İlk önce PlayerScoreboardMoveEditorView ve onun ViewModel, PlayerScoreboardMoveEditorViewModel ile başlayın.

PlayerScoreboardMoveEditorViewModel açın ve özelliklerine bakın.

playerName değişmesi beklenmediğinden, olduğu gibi bırakabilirsiniz.

Diğer beş özellik (beş hareket türü) değişecek, bu yüzden bu konuda bir şeyler yapmanız gerekiyor. Çözüm? Projeye yeni eklediğiniz yukarıda belirtilen Dynamic sınıf.

PlayerScoreboardMoveEditorViewModel içinde, hareket sayılarını temsil eden beş Dize için tanımları kaldırın ve bununla değiştirin:

 var onePointMoveCount: Dynamic<String> { get } var twoPointMoveCount: Dynamic<String> { get } var assistMoveCount: Dynamic<String> { get } var reboundMoveCount: Dynamic<String> { get } var foulMoveCount: Dynamic<String> { get }

ViewModel protokolünün şimdi nasıl görünmesi gerektiği:

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

Bu Dynamic tür, o belirli özelliğin değerini değiştirmenize ve aynı zamanda bu durumda Görünüm olacak olan change-listener nesnesine bildirimde bulunmanıza olanak tanır.

Şimdi, gerçek ViewModel uygulamasını PlayerScoreboardMoveEditorViewModelFromPlayer güncelleyin.

Bunu değiştirin:

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

Takip ederek:

 let onePointMoveCount: Dynamic<String> let twoPointMoveCount: Dynamic<String> let assistMoveCount: Dynamic<String> let reboundMoveCount: Dynamic<String> let foulMoveCount: Dynamic<String>

Not: Asıl özelliği değiştirmeyeceğiniz için bu özellikleri let ile sabitler olarak bildirmekte bir sakınca yoktur. Dynamic nesnesindeki value özelliğini değiştireceksiniz.

Şimdi, Dynamic nesnelerinizi başlatmadığınız için derleme hataları var.

PlayerScoreboardMoveEditorViewModelFromPlayer init yönteminin içinde, taşıma özelliklerinin başlatılmasını şununla değiştirin:

 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))")

makeMove PlayerScoreboardMoveEditorViewModelFromPlayer gidin ve onu aşağıdaki kodla değiştirin:

 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))" }

Gördüğünüz gibi, Dynamic sınıfının örneklerini yarattınız ve ona String değerleri atadınız. Verileri güncellemeniz gerektiğinde Dynamic özelliğin kendisini değiştirmeyin; bunun yerine value özelliğini güncelleyin.

Harika! PlayerScoreboardMoveEditorViewModel artık dinamik.

Bunu kullanalım ve bu değişiklikleri gerçekten dinleyecek olan görünüme gidelim.

PlayerScoreboardMoveEditorView ve onun fillUI yöntemini açın ( Dynamic nesne türüne String değeri atamaya çalıştığınız için bu noktada bu yöntemde derleme hataları görmelisiniz.)

"Hatalı" satırları değiştirin:

 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

Takip ederek:

 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 }

Ardından, hareket eylemlerini temsil eden beş yöntemi uygulayın ( Düğme Eylemi bölümü):

 @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() }

Uygulamayı çalıştırın ve bazı hareket düğmelerine tıklayın. Eylem düğmesine tıkladığınızda oynatıcı görünümlerindeki sayaç değerlerinin nasıl değiştiğini göreceksiniz.

iOS Uygulaması

PlayerScoreboardMoveEditorView ve PlayerScoreboardMoveEditorViewModel ile işiniz bitti.

Bu basitti.

Şimdi aynısını ana görünümünüz için yapmanız gerekiyor ( GameScoreboardEditorViewController ).

İlk önce GameScoreboardEditorViewModel açın ve görünümün yaşam döngüsü boyunca hangi değerlerin değişmesinin beklendiğini görün.

time , score , isFinished , isPaused tanımlarını Dynamic sürümlerle değiştirin:

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

ViewModel uygulamasına gidin ( GameScoreboardEditorViewModelFromGame ) ve protokolde belirtilen özelliklerle aynısını yapın.

Bunu değiştirin:

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

Takip ederek:

 let time: Dynamic<String> let score: Dynamic<String> let isFinished: Dynamic<Bool> let isPaused: Dynamic<Bool>

ViewModel'in türünü String ve Bool Dynamic<String> ve Dynamic<Bool> olarak değiştirdiğiniz için şimdi birkaç hata alacaksınız.

Bunu düzeltelim.

Aşağıdaki ile değiştirerek togglePause yöntemini düzeltin:

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

Tek değişikliğin, mülk değerini artık doğrudan mülk üzerinde ayarlamamanız olduğuna dikkat edin. Bunun yerine, onu nesnenin value özelliğinde ayarlarsınız.

Şimdi, şunu değiştirerek initWithGame yöntemini düzeltin:

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

Takip ederek:

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

Şimdi meseleyi anlamalısın.

String , Int ve Bool gibi ilkel değerleri, bu nesnelerin size hafif bağlama mekanizmasını veren Dynamic<T> sürümleriyle sarıyorsunuz.

Düzeltmeniz gereken bir hata daha var.

startTimer yönteminde hata satırını şununla değiştirin:

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

ViewModel'inizi, oynatıcının ViewModel'inde yaptığınız gibi dinamik olacak şekilde yükselttiniz. Ancak yine de Görünümünüzü güncellemeniz gerekiyor ( GameScoreboardEditorViewController ).

Tüm fillUI yöntemini bununla değiştirin:

 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] }

Tek fark, dört dinamik özelliğinizi değiştirmiş olmanız ve bunların her birine değişiklik dinleyicileri eklemenizdir.

Bu noktada, uygulamanızı çalıştırırsanız, Başlat/Duraklat düğmesi arasında geçiş yapmak oyun zamanlayıcısını başlatır ve duraklatır. Bu, oyun sırasındaki molalar için kullanılır.

Puan düğmelerinden birine ( 1 ve 2 puan düğmesi) bastığınızda, kullanıcı arayüzünde puanın değişmemesi dışında neredeyse bitirdiniz.

Bunun nedeni, temeldeki Game modeli nesnesindeki puan değişikliklerini ViewModel'e kadar gerçekten yaymamış olmanızdır.

Bu nedenle, küçük bir inceleme için Game modeli nesnesini açın. updateScore yöntemini kontrol edin.

 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) }

Bu yöntem iki önemli şey yapar.

İlk olarak, oyun her iki takımın puanlarına göre isFinished özelliğini true olarak ayarlar.

Bundan sonra, puanın değiştiğine dair bir bildirim gönderir. Bu bildirimi GameScoreboardEditorViewModelFromGame dinleyecek ve bildirim işleyici yönteminde dinamik puan değerini güncelleyeceksiniz.

Add this line at the bottom of initWithGame method (don't forget the super.init() call to avoid errors):

 super.init() subscribeToNotifications()

Below initWithGame method, add deinit method, since you want to do the cleanup properly and avoid crashes caused by the NotificationCenter .

 deinit { unsubscribeFromNotifications() }

Finally, add the implementations of these methods. Add this section right below the deinit method:

 // 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 } }

Now, run the app, and click on the player views to change scores. Since you've already connected dynamic score and isFinished in the ViewModel with the View, everything should work when you change the score value inside the ViewModel.

How to Further Improve the App

While there's always room for improvement, this is out of scope of this tutorial.

For example, we do not stop the time automatically when the game is over (when one of the teams reaches 15 points), we just hide the player views.

You can play with the app if you like and upgrade it to have a “game creator” view, which would create a game, assign team names, assign player names and create a Game object that could be used to present GameScoreboardEditorViewController .

We can create another “game list” view that uses the UITableView to show multiple games in progress with some detailed info in the table cell. In cell select, we can show the GameScoreboardEditorViewController with the selected Game .

The GameLibrary has already been implemented. Just remember to pass that library reference to the ViewModel objects in their initializer. For example, “game creator's” ViewModel would need to have an instance of GameLibrary passed through the initializer so that it would be able to insert the created Game object into the library. “Game list's” ViewModel would also need this reference to fetch all games from the library, which will be needed by the UITableView.

The idea is to hide all of the dirty (non-UI) work inside the ViewModel and have the UI (View) only act with prepared presentation data.

What now?

After you get used to the MVVM, you can further improve it by using Uncle Bob's Clean Architecture rules.

An additional good read is a three-part tutorial on Android architecture:

  • Android Architecture: Part 1 – Every new beginning is hard,
  • Android Architecture: Part 2 – The clean architecture,
  • Android Architecture: Part 2 – The clean architecture.

Examples are written in Java (for Android), and if you are familiar with Java (which is much closer to Swift then Objective-C is to Java), you'll get ideas on how to further refactor your code inside the ViewModel objects so that they don't import any iOS modules ( UIKit or CoreLocation eg).

These iOS modules can be hidden behind the pure NSObjects , which is good for code reusability.

MVVM is a good choice for most iOS apps, and hopefully, you will give it a try in your next project. Or, try it in your current project when you are creating a UIViewController .

Related: Working With Static Patterns: A Swift MVVM Tutorial