Tutorial Swift: Pengantar Pola Desain MVVM

Diterbitkan: 2022-03-11

Jadi, Anda memulai proyek iOS baru, Anda menerima dari desainer semua dokumen .pdf dan .sketch yang dibutuhkan, dan Anda sudah memiliki visi tentang bagaimana Anda akan membangun aplikasi baru ini.

Anda mulai mentransfer layar UI dari sketsa desainer ke file ViewController .swift , .xib dan .storyboard .

UITextField di sini, UITableView di sana, beberapa UILabels lagi dan sedikit UIButtons . IBOutlets dan IBActions juga disertakan. Semua baik, kami masih di zona UI.

Namun, inilah saatnya untuk melakukan sesuatu dengan semua elemen UI ini; UIButtons akan menerima sentuhan jari, UILabels dan UITableViews akan membutuhkan seseorang untuk memberi tahu mereka apa yang harus ditampilkan dan dalam format apa.

Tiba-tiba, Anda memiliki lebih dari 3.000 baris kode.

3.000 baris kode Swift

Anda berakhir dengan banyak kode spaghetti.

Langkah pertama untuk mengatasinya adalah dengan menerapkan pola desain Model-View-Controller (MVC). Namun, pola ini memiliki masalah sendiri. Muncullah pola desain Model-View-ViewModel (MVVM) yang menyelamatkan hari.

Berurusan dengan Spaghetti Code

Dalam waktu singkat, ViewController awal Anda menjadi terlalu pintar dan terlalu masif.

Kode jaringan, kode penguraian data, kode penyesuaian data untuk presentasi UI, pemberitahuan status aplikasi, perubahan status UI. Semua kode itu terpenjara di dalam if -ology dari satu file yang tidak dapat digunakan kembali dan hanya akan muat dalam proyek ini.

Kode ViewController Anda telah menjadi kode spaghetti yang terkenal.

Bagaimana itu bisa terjadi?

Kemungkinan alasannya adalah seperti ini:

Anda terburu-buru untuk melihat bagaimana data back-end berperilaku di dalam UITableView , jadi Anda meletakkan beberapa baris kode jaringan di dalam metode temp ViewController hanya untuk mengambil .json itu dari jaringan. Selanjutnya, Anda perlu memproses data di dalam .json , jadi Anda menulis metode temp lain untuk mencapainya. Atau, lebih buruk lagi, Anda melakukannya di dalam metode yang sama.

ViewController terus berkembang ketika kode otorisasi pengguna muncul. Kemudian format data mulai berubah, UI berevolusi dan membutuhkan beberapa perubahan radikal, dan Anda terus menambahkan lebih banyak if s ke dalam if -ology yang sudah masif.

Tapi, kenapa UIViewController menjadi tidak terkendali?

UIViewController adalah tempat yang logis untuk mulai mengerjakan kode UI Anda. Ini mewakili layar fisik yang Anda lihat saat menggunakan aplikasi apa pun dengan perangkat iOS Anda. Bahkan Apple menggunakan UIViewControllers di aplikasi sistem utamanya ketika beralih di antara aplikasi yang berbeda dan UI animasinya.

Apple mendasarkan abstraksi UI-nya di dalam UIViewController , karena merupakan inti dari kode UI iOS dan bagian dari pola desain MVC .

Terkait: 10 Kesalahan Paling Umum yang Tidak Diketahui Pengembang iOS yang Mereka Buat

Meningkatkan ke Pola Desain MVC

Pola Desain MVC

Dalam pola desain MVC, View seharusnya tidak aktif dan hanya menampilkan data yang disiapkan sesuai permintaan.

Controller harus bekerja pada data Model untuk mempersiapkannya untuk Views , yang kemudian menampilkan data tersebut.

View juga bertanggung jawab untuk memberi tahu Controller tentang tindakan apa pun, seperti sentuhan pengguna.

Seperti disebutkan, UIViewController biasanya merupakan titik awal dalam membangun layar UI. Perhatikan bahwa dalam namanya, ini berisi "tampilan" dan "pengontrol". Ini berarti bahwa itu "mengontrol tampilan." Ini tidak berarti bahwa kode "pengontrol" dan "tampilan" harus masuk ke dalam.

Campuran tampilan dan kode pengontrol ini sering terjadi ketika Anda memindahkan IBOutlets dari subview kecil di dalam UIViewController , dan memanipulasi subview tersebut langsung dari UIViewController . Sebagai gantinya, Anda seharusnya membungkus kode itu di dalam subkelas UIView khusus.

Mudah untuk melihat bahwa ini dapat menyebabkan jalur kode View dan Controller disilangkan.

MVVM Untuk Menyelamatkan

Di sinilah pola MVVM berguna.

Karena UIViewController seharusnya menjadi Pengendali dalam pola MVC, dan itu sudah melakukan banyak hal dengan Views , kita dapat menggabungkannya ke dalam View dari pola baru kita - MVVM .

Pola Desain MVVM

Dalam pola desain MVVM, Model sama dengan pola MVC. Ini mewakili data sederhana.

Tampilan diwakili oleh objek UIView atau UIViewController , disertai dengan file .xib dan .storyboard , yang seharusnya hanya menampilkan data yang telah disiapkan. (Kami tidak ingin memiliki kode NSDateFormatter , misalnya, di dalam Tampilan.)

Hanya string sederhana yang diformat yang berasal dari ViewModel .

ViewModel menyembunyikan semua kode jaringan asinkron, kode persiapan data untuk presentasi visual, dan mendengarkan kode untuk perubahan Model . Semua ini tersembunyi di balik model API yang terdefinisi dengan baik agar sesuai dengan View khusus ini.

Salah satu manfaat menggunakan MVVM adalah pengujian. Karena ViewModel adalah NSObject murni (atau struct misalnya), dan tidak digabungkan dengan kode UIKit , Anda dapat mengujinya dengan lebih mudah dalam pengujian unit Anda tanpa memengaruhi kode UI.

Sekarang, View ( UIViewController / UIView ) menjadi lebih sederhana sementara ViewModel bertindak sebagai perekat antara Model dan View .

Menerapkan MVVM Di Swift

MVVM Di Swift

Untuk menunjukkan MVVM beraksi, Anda dapat mengunduh dan memeriksa contoh proyek Xcode yang dibuat untuk tutorial ini di sini. Proyek ini menggunakan Swift 3 dan Xcode 8.1.

Ada dua versi proyek: Pemula dan Selesai.

Versi Selesai adalah aplikasi mini yang lengkap, di mana Pemula adalah proyek yang sama tetapi tanpa metode dan objek yang diimplementasikan.

Pertama, saya sarankan Anda mengunduh proyek Starter , dan ikuti tutorial ini. Jika Anda memerlukan referensi cepat proyek untuk nanti, unduh proyek Selesai .

Pengenalan Proyek Tutorial

Proyek tutorial adalah aplikasi bola basket untuk melacak tindakan pemain selama pertandingan.

Aplikasi bola basket

Ini digunakan untuk pelacakan cepat gerakan pengguna dan skor keseluruhan dalam permainan pikap.

Dua tim bermain sampai skor 15 (dengan setidaknya selisih dua poin) tercapai. Setiap pemain dapat mencetak satu poin hingga dua poin, dan setiap pemain dapat membantu, rebound, dan melakukan pelanggaran.

Hirarki proyek terlihat seperti ini:

Hirarki proyek

Model

  • Game.swift
    • Berisi logika permainan, melacak skor keseluruhan, melacak gerakan setiap pemain.
  • Team.swift
    • Berisi nama tim dan daftar pemain (tiga pemain di setiap tim).
  • Player.swift
    • Seorang pemain tunggal dengan nama.

Melihat

  • HomeViewController.swift
    • Pengontrol tampilan root, yang menghadirkan GameScoreboardEditorViewController
  • GameScoreboardEditorViewController.swift
    • Dilengkapi dengan tampilan Interface Builder di Main.storyboard .
    • Layar yang menarik untuk tutorial ini.
  • PlayerScoreboardMoveEditorView.swift
    • Dilengkapi dengan tampilan Interface Builder di PlayerScoreboardMoveEditorView.xib
    • Subview dari view di atas, juga menggunakan design pattern MVVM.

LihatModel

  • Grup ViewModel kosong, inilah yang akan Anda bangun dalam tutorial ini.

Proyek Xcode yang diunduh sudah berisi placeholder untuk objek View ( UIView dan UIViewController ). Proyek ini juga berisi beberapa objek yang dibuat khusus yang dibuat untuk mendemonstrasikan salah satu cara tentang cara memberikan data ke objek ViewModel ( grup Services ).

Grup Extensions berisi ekstensi yang berguna untuk kode UI yang tidak termasuk dalam cakupan tutorial ini dan sudah cukup jelas.

Jika Anda menjalankan aplikasi pada titik ini, itu akan menampilkan UI yang telah selesai, tetapi tidak ada yang terjadi, ketika pengguna menekan tombol.

Ini karena Anda hanya membuat tampilan dan IBActions tanpa menghubungkannya ke logika aplikasi dan tanpa mengisi elemen UI dengan data dari model (dari objek Game , seperti yang akan kita pelajari nanti).

Menghubungkan Tampilan dan Model dengan ViewModel

Dalam pola desain MVVM, View seharusnya tidak tahu apa-apa tentang Model. Satu-satunya hal yang View tahu adalah bagaimana bekerja dengan ViewModel.

Mulailah dengan memeriksa Tampilan Anda.

Dalam file GameScoreboardEditorViewController.swift , metode fillUI kosong pada saat ini. Ini adalah tempat Anda ingin mengisi UI dengan data. Untuk mencapai ini, Anda perlu menyediakan data untuk ViewController . Anda melakukan ini dengan objek ViewModel.

Pertama, buat objek ViewModel yang berisi semua data yang diperlukan untuk ViewController ini.

Buka grup proyek ViewModel Xcode, yang akan kosong, buat file GameScoreboardEditorViewModel.swift , dan jadikan itu sebagai protokol.

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

Menggunakan protokol seperti ini membuat semuanya tetap bagus dan bersih; Anda hanya harus menentukan data yang akan Anda gunakan.

Selanjutnya, buat implementasi untuk protokol ini.

Buat file baru, bernama GameScoreboardEditorViewModelFromGame.swift , dan jadikan objek ini sebagai subkelas NSObject .

Juga, buat itu sesuai dengan protokol GameScoreboardEditorViewModel :

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

Perhatikan bahwa Anda telah menyediakan semua yang diperlukan agar ViewModel bekerja melalui penginisialisasi.

Anda memberikannya objek Game , yang merupakan Model di bawah ViewModel ini.

Jika Anda menjalankan aplikasi sekarang, itu tetap tidak akan berfungsi karena Anda belum menghubungkan data ViewModel ini ke View itu sendiri.

Jadi, kembali ke file GameScoreboardEditorViewController.swift , dan buat properti publik bernama viewModel .

Buatlah dari jenis GameScoreboardEditorViewModel .

Tempatkan tepat sebelum metode viewDidLoad di dalam GameScoreboardEditorViewController.swift .

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

Selanjutnya, Anda perlu menerapkan metode fillUI .

Perhatikan bagaimana metode ini dipanggil dari dua tempat, pengamat properti viewModel ( didSet ) dan metode viewDidLoad . Ini karena kita dapat membuat ViewController dan menetapkan ViewModel ke dalamnya sebelum melampirkannya ke tampilan (sebelum metode viewDidLoad dipanggil).

Di sisi lain, Anda dapat melampirkan tampilan ViewController ke tampilan lain dan memanggil viewDidLoad , tetapi jika viewModel tidak disetel pada saat itu, tidak akan terjadi apa-apa.

Itu sebabnya pertama-tama, Anda perlu memeriksa apakah semuanya sudah diatur untuk data Anda untuk mengisi UI. Penting untuk menjaga kode Anda dari penggunaan yang tidak terduga.

Jadi, buka metode fillUI , dan ganti dengan kode berikut:

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

Sekarang, terapkan metode pauseButtonPress :

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

Yang perlu Anda lakukan sekarang, adalah mengatur properti viewModel yang sebenarnya pada ViewController ini. Anda melakukan ini "dari luar."

Buka file HomeViewController.swift dan batalkan komentar pada ViewModel; buat dan atur baris dalam metode showGameScoreboardEditorViewController :

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

Sekarang, jalankan aplikasinya. Seharusnya terlihat seperti ini:

Aplikasi iOS

Tampilan tengah, yang bertanggung jawab atas skor, waktu, dan nama tim, tidak lagi menampilkan nilai yang ditetapkan di Pembuat Antarmuka.

Sekarang, ini menunjukkan nilai dari objek ViewModel itu sendiri, yang mendapatkan datanya dari objek Model sebenarnya (Objek Game ).

Bagus sekali! Tapi bagaimana dengan pandangan pemain? Tombol-tombol itu masih tidak melakukan apa-apa.

Anda tahu bahwa Anda memiliki enam tampilan untuk pelacakan gerakan pemain.

Anda membuat subview terpisah, bernama PlayerScoreboardMoveEditorView untuk itu, yang tidak melakukan apa pun dengan data nyata untuk saat ini dan menampilkan nilai statis yang ditetapkan melalui Pembuat Antarmuka di dalam file PlayerScoreboardMoveEditorView.xib .

Anda perlu memberikan beberapa data.

Anda akan melakukannya dengan cara yang sama seperti yang Anda lakukan dengan GameScoreboardEditorViewController dan GameScoreboardEditorViewModel .

Buka grup ViewModel di proyek Xcode, dan tentukan protokol baru di sini.

Buat file baru bernama PlayerScoreboardMoveEditorViewModel.swift , dan masukkan kode berikut di dalamnya:

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

Protokol ViewModel ini dirancang agar sesuai dengan PlayerScoreboardMoveEditorView , seperti yang Anda lakukan di tampilan induk, GameScoreboardEditorViewController .

Anda harus memiliki nilai untuk lima gerakan berbeda yang dapat dilakukan pengguna, dan Anda perlu bereaksi, saat pengguna menyentuh salah satu tombol tindakan. Anda juga memerlukan String untuk nama pemain.

Setelah Anda selesai melakukannya, buat kelas konkret yang mengimplementasikan protokol ini, seperti yang Anda lakukan dengan tampilan induk ( GameScoreboardEditorViewController ).

Selanjutnya, buat implementasi protokol ini: Buat file baru, beri nama PlayerScoreboardMoveEditorViewModelFromPlayer.swift , dan jadikan objek ini sebagai subkelas NSObject . Juga, buat itu sesuai dengan protokol PlayerScoreboardMoveEditorViewModel :

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

Sekarang, Anda perlu memiliki objek yang akan membuat instance ini "dari luar", dan mengaturnya sebagai properti di dalam PlayerScoreboardMoveEditorView .

Ingat bagaimana HomeViewController bertanggung jawab untuk menyetel properti viewModel pada GameScoreboardEditorViewController ?

Dengan cara yang sama, GameScoreboardEditorViewController adalah tampilan induk dari PlayerScoreboardMoveEditorView Anda dan bahwa GameScoreboardEditorViewController akan bertanggung jawab untuk membuat objek PlayerScoreboardMoveEditorViewModel .

Anda perlu memperluas GameScoreboardEditorViewModel Anda terlebih dahulu.

Buka GameScoreboardEditorViewMode l dan tambahkan dua properti ini:

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

Juga, perbarui GameScoreboardEditorViewModelFromGame dengan dua properti ini tepat di atas metode initWithGame :

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

Tambahkan dua baris ini di dalam initWithGame :

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

Dan tentu saja, tambahkan metode playerViewModelsWithPlayers yang hilang:

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

Besar!

Anda telah memperbarui ViewModel ( GameScoreboardEditorViewModel ) Anda dengan susunan pemain tuan rumah dan tandang. Anda masih perlu mengisi dua array ini.

Anda akan melakukannya di tempat yang sama saat Anda menggunakan viewModel ini untuk mengisi UI.

Buka GameScoreboardEditorViewController dan buka metode fillUI . Tambahkan baris ini di akhir metode:

 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]

Untuk saat ini, Anda memiliki kesalahan pembuatan karena Anda tidak menambahkan properti viewModel yang sebenarnya di dalam PlayerScoreboardMoveEditorView .

Tambahkan kode berikut di atas init method inside the PlayerScoreboardMoveEditorView`.

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

Dan terapkan metode fillUI :

 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 }

Terakhir, jalankan aplikasi, dan lihat bagaimana data dalam elemen UI adalah data sebenarnya dari objek Game .

Aplikasi iOS

Pada titik ini, Anda memiliki aplikasi fungsional yang menggunakan pola desain MVVM.

Ini menyembunyikan Model dengan baik dari Tampilan, dan Tampilan Anda jauh lebih sederhana daripada yang biasa Anda lakukan dengan MVC.

Hingga saat ini, Anda telah membuat aplikasi yang berisi View dan ViewModel-nya.

Tampilan itu juga memiliki enam contoh subview yang sama (tampilan pemutar) dengan ViewModel-nya.

Namun, seperti yang mungkin Anda perhatikan, Anda hanya dapat menampilkan data di UI sekali (dalam metode fillUI ), dan data tersebut statis.

Jika data Anda dalam tampilan tidak akan berubah selama masa tampilan itu, maka Anda memiliki solusi yang baik dan bersih untuk menggunakan MVVM dengan cara ini.

Membuat ViewModel Dinamis

Karena data Anda akan berubah, Anda perlu membuat ViewModel Anda dinamis.

Artinya, ketika Model berubah, ViewModel harus mengubah nilai properti publiknya; itu akan menyebarkan perubahan kembali ke tampilan, yang merupakan salah satu yang akan memperbarui UI.

Ada banyak cara untuk melakukan ini.

Saat Model berubah, ViewModel akan diberi tahu terlebih dahulu.

Anda memerlukan beberapa mekanisme untuk menyebarkan perubahan apa saja ke View.

Beberapa opsi termasuk RxSwift, yang merupakan perpustakaan yang cukup besar dan membutuhkan waktu untuk membiasakan diri.

ViewModel dapat mengaktifkan NSNotification s pada setiap perubahan nilai properti, tetapi ini menambahkan banyak kode yang memerlukan penanganan tambahan, seperti berlangganan notifikasi dan berhenti berlangganan saat tampilan dibatalkan alokasinya.

Key-Value-Observing (KVO) adalah opsi lain, tetapi pengguna akan mengonfirmasi bahwa API-nya tidak mewah.

Dalam tutorial ini, Anda akan menggunakan generik Swift dan penutupan, yang dijelaskan dengan baik di artikel Bindings, Generics, Swift dan MVVM.

Sekarang, mari kembali ke contoh aplikasi.

Buka grup proyek ViewModel, dan buat file Swift baru, 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 } }

Anda akan menggunakan kelas ini untuk properti di ViewModels yang ingin Anda ubah selama siklus hidup Tampilan.

Pertama, mulai dengan PlayerScoreboardMoveEditorView dan ViewModel-nya, PlayerScoreboardMoveEditorViewModel .

Buka PlayerScoreboardMoveEditorViewModel dan lihat propertinya.

Karena nama playerName tidak diharapkan berubah, Anda dapat membiarkannya apa adanya.

Lima properti lainnya (lima jenis gerakan) akan berubah, jadi Anda perlu melakukan sesuatu tentang itu. Solusinya? Kelas Dynamic yang disebutkan di atas yang baru saja Anda tambahkan ke proyek.

Di dalam PlayerScoreboardMoveEditorViewModel hapus definisi untuk lima String yang mewakili jumlah gerakan dan ganti dengan ini:

 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 }

Beginilah tampilan protokol ViewModel sekarang:

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

Jenis Dynamic ini memungkinkan Anda untuk mengubah nilai properti tertentu, dan pada saat yang sama, memberi tahu objek change-listener, yang, dalam hal ini, akan menjadi Tampilan.

Sekarang, perbarui implementasi ViewModel PlayerScoreboardMoveEditorViewModelFromPlayer yang sebenarnya.

Ganti ini:

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

dengan berikut ini:

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

Catatan: Tidak apa-apa untuk mendeklarasikan properti ini sebagai konstanta dengan let karena Anda tidak akan mengubah properti sebenarnya. Anda akan mengubah properti value pada objek Dynamic .

Sekarang, ada kesalahan pembuatan karena Anda tidak menginisialisasi objek Dynamic Anda.

Di dalam metode init PlayerScoreboardMoveEditorViewModelFromPlayer , ganti inisialisasi properti pemindahan dengan ini:

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

Di dalam PlayerScoreboardMoveEditorViewModelFromPlayer buka metode makeMove , dan ganti dengan kode berikut:

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

Seperti yang Anda lihat, Anda telah membuat instance kelas Dynamic , dan menetapkannya nilai String . Saat Anda perlu memperbarui data, jangan ubah properti Dynamic itu sendiri; alih-alih perbarui properti value .

Besar! PlayerScoreboardMoveEditorViewModel sekarang dinamis.

Mari kita manfaatkan, dan buka tampilan yang benar-benar akan mendengarkan perubahan ini.

Buka PlayerScoreboardMoveEditorView dan metode fillUI -nya (Anda akan melihat kesalahan build dalam metode ini saat ini karena Anda mencoba menetapkan nilai String ke tipe objek Dynamic .)

Ganti baris "yang salah":

 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

dengan berikut ini:

 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 }

Selanjutnya, terapkan lima metode yang mewakili tindakan pindah (bagian Tindakan Tombol ):

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

Jalankan aplikasi, dan klik beberapa tombol pindah. Anda akan melihat bagaimana nilai penghitung di dalam tampilan pemutar berubah saat Anda mengklik tombol aksi.

Aplikasi iOS

Anda telah selesai dengan PlayerScoreboardMoveEditorView dan PlayerScoreboardMoveEditorViewModel .

Ini sederhana.

Sekarang, Anda perlu melakukan hal yang sama dengan tampilan utama Anda ( GameScoreboardEditorViewController ).

Pertama, buka GameScoreboardEditorViewModel dan lihat nilai mana yang diharapkan berubah selama siklus hidup tampilan.

Ganti definisi time , score , isFinished , isPaused dengan versi Dynamic :

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

Buka implementasi ViewModel ( GameScoreboardEditorViewModelFromGame ) dan lakukan hal yang sama dengan properti yang dideklarasikan dalam protokol.

Ganti ini:

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

dengan berikut ini:

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

Anda akan mendapatkan beberapa kesalahan, sekarang, karena Anda mengubah tipe ViewModel dari String dan Bool menjadi Dynamic<String> dan Dynamic<Bool> .

Mari kita perbaiki itu.

Perbaiki metode togglePause dengan menggantinya dengan yang berikut ini:

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

Perhatikan bagaimana satu-satunya perubahan adalah Anda tidak lagi menetapkan nilai properti secara langsung pada properti. Sebagai gantinya, Anda mengaturnya pada properti value objek.

Sekarang, perbaiki metode initWithGame dengan mengganti ini:

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

dengan berikut ini:

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

Anda harus mendapatkan intinya sekarang.

Anda membungkus nilai primitif, seperti String , Int dan Bool , dengan versi Dynamic<T> dari objek tersebut, yang memberi Anda mekanisme pengikatan yang ringan.

Anda memiliki satu kesalahan lagi untuk diperbaiki.

Dalam metode startTimer , ganti baris kesalahan dengan:

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

Anda telah meningkatkan ViewModel menjadi dinamis, seperti yang Anda lakukan dengan ViewModel pemutar. Tetapi Anda masih perlu memperbarui Tampilan Anda ( GameScoreboardEditorViewController ).

Ganti seluruh metode fillUI dengan ini:

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

Satu-satunya perbedaan adalah Anda mengubah empat properti dinamis dan menambahkan pendengar perubahan ke masing-masing properti.

Pada titik ini, jika Anda menjalankan aplikasi, mengaktifkan tombol Mulai/Jeda akan memulai dan menjeda pengatur waktu game. Ini digunakan untuk time-out selama pertandingan.

Anda hampir selesai kecuali skor tidak berubah di UI, ketika Anda menekan salah satu tombol titik ( tombol 1 dan 2 poin).

Ini karena Anda belum benar-benar menyebarkan perubahan skor di objek model Game yang mendasarinya hingga ViewModel.

Jadi, buka objek model Game untuk sedikit pemeriksaan. Periksa metode updateScore -nya.

 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 ini melakukan dua hal penting.

Pertama, ini menetapkan properti isFinished menjadi true jika permainan selesai berdasarkan skor kedua tim.

Setelah itu, ia memposting pemberitahuan bahwa skor telah berubah. Anda akan mendengarkan notifikasi ini di GameScoreboardEditorViewModelFromGame dan memperbarui nilai skor dinamis dalam metode handler notifikasi.

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.

Apa sekarang?

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