บทช่วยสอน Swift: บทนำสู่รูปแบบการออกแบบ MVVM
เผยแพร่แล้ว: 2022-03-11ดังนั้น คุณกำลังเริ่มต้นโครงการ iOS ใหม่ คุณได้รับเอกสาร .pdf
และ . .sketch
ที่จำเป็นทั้งหมดจากนักออกแบบ และคุณมีวิสัยทัศน์อยู่แล้วเกี่ยวกับวิธีสร้างแอปใหม่นี้
คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยังไฟล์ ViewController
.swift
, .xib
และ . .storyboard
ของคุณ
UITextField
ที่นี่ UITableView
ที่นั่น UILabels
อีกสองสามตัวและ UIButtons
เล็กน้อย IBOutlets
และ IBActions
รวมอยู่ด้วย ดีแล้วที่เรายังอยู่ในโซน UI
อย่างไรก็ตาม ถึงเวลาที่จะทำบางสิ่งกับองค์ประกอบ UI เหล่านี้ทั้งหมด UIButtons
จะได้รับการสัมผัสด้วยนิ้ว UILabels
และ UITableViews
จะต้องมีใครสักคนบอกสิ่งที่จะแสดงและในรูปแบบใด
ทันใดนั้น คุณมีโค้ดมากกว่า 3,000 บรรทัด
คุณลงเอยด้วยรหัสสปาเก็ตตี้มากมาย
ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้รูปแบบการออกแบบ Model-View-Controller (MVC) อย่างไรก็ตาม รูปแบบนี้มีปัญหาของตัวเอง มีรูปแบบการออกแบบ Model-View-ViewModel (MVVM) ที่ช่วยประหยัดเวลาได้
การจัดการกับรหัสสปาเก็ตตี้
ในเวลาไม่นาน ViewController
เริ่มต้นของคุณก็ฉลาดและใหญ่เกินไป
รหัสเครือข่าย รหัสแยกวิเคราะห์ข้อมูล รหัสการปรับข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะของแอป การเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดนั้นถูกกักขังอยู่ภายใน if
-ology ของไฟล์เดียวที่ไม่สามารถนำกลับมาใช้ใหม่และจะพอดีกับโครงการนี้เท่านั้น
รหัส ViewController
ของคุณได้กลายเป็นรหัสปาเก็ตตี้ที่น่าอับอาย
มันเกิดขึ้นได้อย่างไร?
เหตุผลน่าจะเป็นดังนี้:
คุณรีบเร่งที่จะดูว่าข้อมูลส่วนหลังทำงานอย่างไรใน UITableView
ดังนั้นคุณจึงใส่โค้ดเครือข่ายสองสามบรรทัดในวิธี temp ของ ViewController
เพื่อดึง . .json
นั้นจากเครือข่าย ถัดไป คุณต้องประมวลผลข้อมูลภายใน . .json
นั้น คุณจึงเขียนวิธี temp อีกวิธีหนึ่งเพื่อให้สำเร็จ หรือที่แย่ไปกว่านั้นคือ คุณทำแบบนั้นด้วยวิธีเดียวกัน
ViewController
เติบโตขึ้นเรื่อยๆ เมื่อรหัสการให้สิทธิ์ผู้ใช้มาพร้อม จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรง และคุณก็แค่เพิ่มมากขึ้นเรื่อยๆ if
เป็น if
-ology ที่ใหญ่โตอยู่แล้ว
แต่ทำไม UIViewController
ถึงไม่อยู่ในมือ?
UIViewController
เป็นสถานที่ที่เหมาะสมในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใดๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers
ในแอพระบบหลักเมื่อสลับไปมาระหว่างแอพต่างๆ และ UI แบบเคลื่อนไหว
Apple ใช้ UI ที่เป็นนามธรรมภายใน UIViewController
เนื่องจากเป็นแกนหลักของรหัส iOS UI และเป็นส่วนหนึ่งของรูปแบบการออกแบบ MVC
การอัพเกรดเป็นรูปแบบการออกแบบ MVC
ในรูปแบบการออกแบบ MVC มุมมอง ควรจะปิดใช้งานและแสดงเฉพาะข้อมูลที่เตรียมไว้ตามต้องการ
Controller ควรทำงานกับข้อมูล Model เพื่อเตรียมข้อมูลสำหรับ Views ซึ่งจะแสดงข้อมูลนั้น
มุมมอง ยังรับผิดชอบในการแจ้งผู้ ควบคุม เกี่ยวกับการดำเนินการใดๆ เช่น การสัมผัสของผู้ใช้
ดังที่ได้กล่าวมาแล้ว UIViewController
มักจะเป็นจุดเริ่มต้นในการสร้างหน้าจอ UI สังเกตว่าในชื่อของมัน มีทั้ง "มุมมอง" และ "ตัวควบคุม" ซึ่งหมายความว่า "ควบคุมมุมมอง" ไม่ได้หมายความว่าทั้งโค้ด "คอนโทรลเลอร์" และ "มุมมอง" ควรเข้าไปข้างใน
การผสมผสานของรหัสมุมมองและตัวควบคุมนี้มักเกิดขึ้นเมื่อคุณย้าย IBOutlets
ของมุมมองย่อยเล็กๆ ภายใน UIViewController
และจัดการการดูย่อยเหล่านั้นโดยตรงจาก UIViewController
คุณควรรวมโค้ดนั้นไว้ในคลาสย่อย UIView
ที่กำหนดเองแทน
สังเกตได้ง่ายว่าสิ่งนี้อาจนำไปสู่การข้ามเส้นทางของรหัสดูและตัวควบคุม
MVVM สู่การช่วยเหลือ
นี่คือจุดที่รูปแบบ MVVM มีประโยชน์
เนื่องจาก UIViewController
ควรจะเป็น Controller ในรูปแบบ MVC และได้ดำเนินการหลายอย่างกับ Views แล้ว เราจึงสามารถรวมมันเข้ากับ View ของรูปแบบใหม่ของเรา - MVVM
ในรูปแบบการออกแบบ MVVM รุ่น จะเหมือนกับในรูปแบบ MVC มันแสดงถึงข้อมูลอย่างง่าย
มุมมอง แสดงโดยอ็อบเจ็กต์ UIView
หรือ UIViewController
พร้อมด้วยไฟล์ . .xib
และ .storyboard
ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้เท่านั้น (เราไม่ต้องการให้มีโค้ด NSDateFormatter
เช่น ใน View)
เฉพาะสตริงที่มีรูปแบบเรียบง่ายซึ่งมาจาก ViewModel
ViewModel ซ่อนโค้ดเครือข่ายแบบอะซิงโครนัสทั้งหมด รหัสการเตรียมข้อมูลสำหรับการนำเสนอด้วยภาพ และการฟังโค้ดสำหรับการเปลี่ยนแปลง โมเดล ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้พอดีกับ View นี้โดยเฉพาะ
ข้อดีอย่างหนึ่งของการใช้ MVVM คือการทดสอบ เนื่องจาก ViewModel เป็น NSObject
ล้วนๆ (หรือ struct
เป็นต้น) และไม่ได้ใช้ร่วมกับรหัส UIKit
คุณจึงสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยโดยไม่ส่งผลต่อโค้ด UI
ตอนนี้ View ( UIViewController
/ UIView
) นั้นง่ายกว่ามาก ในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่าง Model และ View
การใช้ MVVM ใน Swift
หากต้องการแสดงการใช้งาน MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโปรเจ็กต์ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ได้ที่นี่ โปรเจ็กต์นี้ใช้ Swift 3 และ Xcode 8.1
โครงการมีสองเวอร์ชัน: Starter และ Finish
เวอร์ชันที่ เสร็จสิ้นแล้ว คือแอปพลิเคชันขนาดเล็กที่เสร็จสมบูรณ์ โดยที่ Starter เป็นโปรเจ็กต์เดียวกัน แต่ไม่มีเมธอดและอ็อบเจ็กต์ที่นำมาใช้
ก่อนอื่น ขอแนะนำให้คุณดาวน์โหลดโครงการ Starter และทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโปรเจ็กต์ในภายหลัง ให้ดาวน์โหลดโปรเจ็กต์ที่ เสร็จสิ้น แล้ว
บทแนะนำโครงการ
โปรเจ็กต์การสอนเป็นแอปพลิเคชั่นบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม
ใช้สำหรับติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนโดยรวมในเกมรถกระบะ
สองทีมเล่นจนกว่าจะถึงคะแนน 15 (โดยมีความแตกต่างอย่างน้อยสองคะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้ม และผู้เล่นแต่ละคนสามารถช่วยเหลือ รีบาวน์ และฟาล์วได้
ลำดับชั้นของโปรเจ็กต์มีลักษณะดังนี้:
แบบอย่าง
-
Game.swift
- ประกอบด้วยตรรกะของเกม ติดตามคะแนนโดยรวม ติดตามการเคลื่อนไหวของผู้เล่นแต่ละคน
-
Team.swift
- ประกอบด้วยชื่อทีมและรายชื่อผู้เล่น (ผู้เล่นสามคนในแต่ละทีม)
-
Player.swift
- ผู้เล่นคนเดียวที่มีชื่อ
ดู
-
HomeViewController.swift
- ตัวควบคุมมุมมองรูทซึ่งนำเสนอ
GameScoreboardEditorViewController
- ตัวควบคุมมุมมองรูทซึ่งนำเสนอ
-
GameScoreboardEditorViewController.swift
- เสริมด้วยมุมมองตัวสร้างส่วนต่อประสานใน
Main.storyboard
- หน้าจอที่น่าสนใจสำหรับบทช่วยสอนนี้
- เสริมด้วยมุมมองตัวสร้างส่วนต่อประสานใน
-
PlayerScoreboardMoveEditorView.swift
- เสริมด้วยมุมมองตัวสร้างส่วนต่อประสานใน
PlayerScoreboardMoveEditorView.xib
- มุมมองย่อยของมุมมองด้านบน ยังใช้รูปแบบการออกแบบ MVVM
- เสริมด้วยมุมมองตัวสร้างส่วนต่อประสานใน
ดูรุ่น
- กลุ่ม
ViewModel
ว่างเปล่า นี่คือสิ่งที่คุณจะต้องสร้างในบทช่วยสอนนี้
โปรเจ็กต์ Xcode ที่ดาวน์โหลดมีตัวยึดสำหรับออบเจ็กต์ View แล้ว ( UIView
และ UIViewController
) โปรเจ็กต์ยังมีออบเจ็กต์ที่สร้างขึ้นเองเพื่อสาธิตวิธีใดวิธีหนึ่งในการให้ข้อมูลแก่ออบเจ็กต์ ViewModel (กลุ่ม Services
)
กลุ่ม Extensions
มีส่วนขยายที่เป็นประโยชน์สำหรับรหัส UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง
หากคุณเรียกใช้แอป ณ จุดนี้ จะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม
เนื่องจากคุณได้สร้างเฉพาะมุมมองและ IBActions
โดยไม่ได้เชื่อมต่อกับตรรกะของแอป และไม่มีการเติมองค์ประกอบ UI ด้วยข้อมูลจากแบบจำลอง (จากวัตถุ Game
ดังที่เราจะได้เรียนรู้ในภายหลัง)
การเชื่อมต่อ View และ Model ด้วย ViewModel
ในรูปแบบการออกแบบ MVVM มุมมองไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ View รู้คือวิธีการทำงานกับ ViewModel
เริ่มต้นด้วยการตรวจสอบมุมมองของคุณ
ในไฟล์ GameScoreboardEditorViewController.swift
วิธีการ fillUI
จะว่างเปล่า ณ จุดนี้ นี่คือที่ที่คุณต้องการเติมข้อมูลใน UI เพื่อให้บรรลุสิ่งนี้ คุณต้องจัดเตรียมข้อมูลสำหรับ ViewController
คุณทำเช่นนี้กับวัตถุ ViewModel
ขั้นแรก สร้างวัตถุ ViewModel ที่มีข้อมูลที่จำเป็นทั้งหมดสำหรับ ViewController
นี้
ไปที่กลุ่มโปรเจ็กต์ ViewModel Xcode ซึ่งจะว่างเปล่า สร้างไฟล์ GameScoreboardEditorViewModel.swift
และสร้างเป็นโปรโตคอล
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(); }
การใช้โปรโตคอลแบบนี้ทำให้ทุกอย่างดูดีและสะอาด คุณต้องกำหนดข้อมูลที่คุณจะใช้เท่านั้น
ถัดไป สร้างการใช้งานสำหรับโปรโตคอลนี้
สร้างไฟล์ใหม่ชื่อ GameScoreboardEditorViewModelFromGame.swift
และทำให้วัตถุนี้เป็นคลาสย่อยของ NSObject
นอกจากนี้ ให้สอดคล้องกับโปรโตคอล 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)") } }
โปรดสังเกตว่าคุณได้จัดเตรียมทุกอย่างที่จำเป็นสำหรับ ViewModel เพื่อทำงานผ่านตัวเริ่มต้น
คุณระบุวัตถุ Game
ซึ่งเป็นโมเดลที่อยู่ใต้ ViewModel นี้
หากคุณเรียกใช้แอปตอนนี้ แอปจะยังใช้งานไม่ได้เนื่องจากคุณไม่ได้เชื่อมต่อข้อมูล ViewModel นี้กับ View เอง
ดังนั้น กลับไปที่ไฟล์ GameScoreboardEditorViewController.swift
และสร้างคุณสมบัติสาธารณะชื่อ viewModel
ทำให้เป็นประเภท GameScoreboardEditorViewModel
วางไว้ก่อนเมธอด viewDidLoad
ภายใน GameScoreboardEditorViewController.swift
var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }
ถัดไป คุณต้องใช้เมธอด fillUI
สังเกตว่าวิธีการนี้ถูกเรียกจากสองแห่งอย่างไร ผู้สังเกตการณ์คุณสมบัติ viewModel
( didSet
) และเมธอด viewDidLoad
นี่เป็นเพราะว่าเราสามารถสร้าง ViewController
และกำหนด ViewModel ให้กับมันก่อนที่จะแนบเข้ากับมุมมอง (ก่อนที่จะเรียกเมธอด viewDidLoad
)
ในทางกลับกัน คุณสามารถแนบมุมมองของ ViewController กับอีกมุมมองหนึ่งและเรียก viewDidLoad
ได้ แต่ถ้าไม่ได้ตั้งค่า viewModel
ในขณะนั้น จะไม่มีอะไรเกิดขึ้น
นั่นเป็นเหตุผลก่อนอื่น คุณต้องตรวจสอบว่าทุกอย่างถูกตั้งค่าสำหรับข้อมูลของคุณเพื่อเติม UI หรือไม่ สิ่งสำคัญคือต้องปกป้องโค้ดของคุณจากการใช้งานที่ไม่คาดคิด
ไปที่วิธีการ fillUI
และแทนที่ด้วยรหัสต่อไปนี้:
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) }
ตอนนี้ใช้เมธอด pauseButtonPress
:
@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }
สิ่งที่คุณต้องทำตอนนี้คือตั้งค่าคุณสมบัติ viewModel
จริงบน ViewController
นี้ คุณทำสิ่งนี้ "จากภายนอก"
เปิดไฟล์ HomeViewController.swift
และยกเลิกการใส่เครื่องหมาย ViewModel สร้างและตั้งค่าบรรทัดในวิธี showGameScoreboardEditorViewController
:
// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel
ตอนนี้ เรียกใช้แอป ควรมีลักษณะดังนี้:
มุมมองตรงกลาง ซึ่งรับผิดชอบคะแนน เวลา และชื่อทีม จะไม่แสดงค่าที่ตั้งไว้ในตัวสร้างอินเทอร์เฟซอีกต่อไป
ตอนนี้มันกำลังแสดงค่าจากวัตถุ ViewModel ซึ่งรับข้อมูลจากวัตถุ Model จริง (วัตถุ Game
)
ยอดเยี่ยม! แต่สิ่งที่เกี่ยวกับมุมมองของผู้เล่น? ปุ่มเหล่านั้นยังคงทำอะไรไม่ได้
คุณรู้ว่าคุณมีหกมุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น
คุณได้สร้างมุมมองย่อยแยกต่างหาก ชื่อ PlayerScoreboardMoveEditorView
สำหรับสิ่งนั้น ซึ่งไม่ได้ทำอะไรกับข้อมูลจริงในตอนนี้ และแสดงค่าคงที่ที่ตั้งค่าผ่านตัวสร้างอินเทอร์เฟซภายในไฟล์ PlayerScoreboardMoveEditorView.xib
คุณต้องให้ข้อมูลบางอย่าง
คุณจะทำแบบเดียวกับที่คุณทำกับ GameScoreboardEditorViewController
และ GameScoreboardEditorViewModel
เปิดกลุ่ม ViewModel ในโครงการ Xcode และกำหนดโปรโตคอลใหม่ที่นี่
สร้างไฟล์ใหม่ชื่อ PlayerScoreboardMoveEditorViewModel.swift
และใส่รหัสต่อไปนี้เข้าไป:
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() }
โปรโตคอล ViewModel นี้ได้รับการออกแบบเพื่อให้พอดีกับ PlayerScoreboardMoveEditorView
ของคุณ เช่นเดียวกับที่คุณทำในมุมมองหลัก GameScoreboardEditorViewController
คุณต้องมีค่าสำหรับการเคลื่อนไหวที่แตกต่างกันห้าแบบที่ผู้ใช้สามารถทำได้ และคุณจำเป็นต้องตอบสนองเมื่อผู้ใช้แตะปุ่มการทำงานปุ่มใดปุ่มหนึ่ง คุณต้องมี String
สำหรับชื่อผู้เล่นด้วย
หลังจากที่คุณทำเสร็จแล้ว ให้สร้างคลาสที่เป็นรูปธรรมที่ใช้โปรโตคอลนี้ เช่นเดียวกับที่คุณทำกับมุมมองพาเรนต์ ( GameScoreboardEditorViewController
)
ถัดไป สร้างการใช้งานโปรโตคอลนี้: สร้างไฟล์ใหม่ ตั้งชื่อเป็น PlayerScoreboardMoveEditorViewModelFromPlayer.swift
และทำให้วัตถุนี้เป็นคลาสย่อยของ NSObject
นอกจากนี้ ให้สอดคล้องกับโปรโตคอล 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))" } }
ตอนนี้ คุณต้องมีวัตถุที่จะสร้างอินสแตนซ์นี้ “จากภายนอก” และตั้งค่าเป็นคุณสมบัติภายใน PlayerScoreboardMoveEditorView
จำได้ไหมว่า HomeViewController
รับผิดชอบในการตั้งค่าคุณสมบัติ viewModel
บน GameScoreboardEditorViewController
อย่างไร
ในทำนองเดียวกัน GameScoreboardEditorViewController
เป็นมุมมองหลักของ PlayerScoreboardMoveEditorView
ของคุณและ GameScoreboardEditorViewController
จะรับผิดชอบในการสร้างวัตถุ PlayerScoreboardMoveEditorViewModel
คุณต้องขยาย GameScoreboardEditorViewModel
ของคุณก่อน
เปิด GameScoreboardEditorViewMode
l และเพิ่มคุณสมบัติทั้งสองนี้:
var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }
นอกจากนี้ ให้อัปเดต GameScoreboardEditorViewModelFromGame
ด้วยคุณสมบัติทั้งสองนี้ที่อยู่เหนือเมธอด initWithGame
:
let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]
เพิ่มสองบรรทัดเหล่านี้ใน initWithGame
:
self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)
และแน่นอน เพิ่มเมธอด playerViewModelsWithPlayers
ที่หายไป:

// 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 }
ยอดเยี่ยม!
คุณได้อัปเดต ViewModel ของคุณ ( GameScoreboardEditorViewModel
) ด้วยอาร์เรย์ผู้เล่นในบ้านและนอกบ้าน คุณยังต้องกรอกสองอาร์เรย์นี้
คุณจะทำสิ่งนี้ในที่เดียวกับที่คุณใช้ viewModel
นี้เพื่อเติม UI
เปิด GameScoreboardEditorViewController
และไปที่วิธี fillUI
เพิ่มบรรทัดเหล่านี้เมื่อสิ้นสุดเมธอด:
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]
ในขณะนี้ คุณมีข้อผิดพลาดในการสร้างเนื่องจากคุณไม่ได้เพิ่มคุณสมบัติ viewModel
จริงใน PlayerScoreboardMoveEditorView
เพิ่มโค้ดต่อไปนี้เหนือ init method inside the
PlayerScoreboardMoveEditorView`
var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }
และใช้วิธี 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 }
สุดท้าย เรียกใช้แอป และดูว่าข้อมูลในองค์ประกอบ UI เป็นข้อมูลจริงจากวัตถุ Game
อย่างไร
ณ จุดนี้ คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM
มันซ่อน Model ไว้อย่างดีจาก View และ View ของคุณนั้นง่ายกว่าที่คุณเคยใช้กับ MVC
ถึงจุดนี้ คุณได้สร้างแอปที่มีมุมมองและ ViewModel
มุมมองนั้นยังมีมุมมองย่อยเดียวกันหกกรณี (มุมมองของผู้เล่น) กับ ViewModel
อย่างไรก็ตาม ตามที่คุณอาจสังเกตเห็น คุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI
) และข้อมูลนั้นจะเป็นแบบคงที่
หากข้อมูลของคุณในมุมมองจะไม่เปลี่ยนแปลงตลอดอายุของมุมมองนั้น แสดงว่าคุณมีโซลูชันที่ดีและสะอาดในการใช้ MVVM ในลักษณะนี้
การทำให้ ViewModel เป็นไดนามิก
เนื่องจากข้อมูลของคุณจะเปลี่ยนไป คุณต้องทำให้ ViewModel เป็นไดนามิก
สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนไป ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะของมัน มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นส่วนที่จะอัปเดต UI
มีหลายวิธีในการทำเช่นนี้
เมื่อ Model เปลี่ยนไป ViewModel จะได้รับการแจ้งเตือนก่อน
คุณต้องมีกลไกบางอย่างในการเผยแพร่สิ่งที่เปลี่ยนแปลงไปในมุมมอง
ตัวเลือกบางตัวรวมถึง RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และใช้เวลาในการทำความคุ้นเคย
ViewModel อาจเริ่มการทำงานของ NSNotification
ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละรายการ แต่สิ่งนี้จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติม เช่น การสมัครรับการแจ้งเตือนและยกเลิกการสมัครเมื่อมุมมองได้รับการจัดสรรคืน
Key-Value-Observing (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของ API นั้นไม่ได้แฟนซี
ในบทช่วยสอนนี้ คุณจะใช้ Swift generics และ closures ซึ่งมีการอธิบายไว้อย่างดีในบทความ Bindings, Generics, Swift และ MVVM
ตอนนี้ กลับมาที่แอปตัวอย่างกัน
ไปที่กลุ่มโปรเจ็กต์ ViewModel และสร้างไฟล์ Swift ใหม่ 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 } }
คุณจะใช้คลาสนี้สำหรับคุณสมบัติใน ViewModels ที่คุณคาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตการดู
ขั้นแรก เริ่มต้นด้วย PlayerScoreboardMoveEditorView
และ ViewModel, PlayerScoreboardMoveEditorViewModel
เปิด PlayerScoreboardMoveEditorViewModel
และดูคุณสมบัติของมัน
เนื่องจาก playerName
ไม่คาดว่าจะเปลี่ยนชื่อผู้เล่น คุณจึงปล่อยไว้ตามเดิมได้
คุณสมบัติอื่นๆ อีกห้ารายการ (ห้าประเภทการย้าย) จะเปลี่ยนไป ดังนั้นคุณต้องดำเนินการบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? คลาส Dynamic
ที่กล่าวถึงข้างต้นที่คุณเพิ่งเพิ่มในโครงการ
ภายใน PlayerScoreboardMoveEditorViewModel
ลบคำจำกัดความสำหรับห้าสตริงที่แสดงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:
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 ควรมีลักษณะดังนี้:
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() }
ประเภท Dynamic
นี้ทำให้คุณสามารถเปลี่ยนค่าของคุณสมบัติเฉพาะนั้น และในขณะเดียวกัน ให้แจ้งออบเจกต์ change-listener ซึ่งในกรณีนี้ จะเป็นมุมมอง
ตอนนี้ อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer
แทนที่สิ่งนี้:
var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String
ด้วยสิ่งต่อไปนี้:
let onePointMoveCount: Dynamic<String> let twoPointMoveCount: Dynamic<String> let assistMoveCount: Dynamic<String> let reboundMoveCount: Dynamic<String> let foulMoveCount: Dynamic<String>
หมายเหตุ: เป็นเรื่องปกติที่จะประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย let
เนื่องจากคุณจะไม่เปลี่ยนคุณสมบัติจริง คุณจะเปลี่ยนคุณสมบัติของ value
บนวัตถุ Dynamic
ขณะนี้ มีข้อผิดพลาดในการสร้างเนื่องจากคุณไม่ได้เริ่มต้นวัตถุ Dynamic
ของคุณ
ภายในวิธีการเริ่มต้นของ PlayerScoreboardMoveEditorViewModelFromPlayer
ให้แทนที่การเริ่มต้นคุณสมบัติการย้ายด้วยสิ่งนี้:
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))")
ภายใน PlayerScoreboardMoveEditorViewModelFromPlayer
ไปที่เมธอด makeMove
และแทนที่ด้วยรหัสต่อไปนี้:
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))" }
อย่างที่คุณเห็น คุณได้สร้างอินสแตนซ์ของคลาส Dynamic
และกำหนดค่า String
ให้กับมัน เมื่อคุณต้องการอัปเดตข้อมูล อย่าเปลี่ยนคุณสมบัติ Dynamic
เอง ค่อนข้างปรับปรุงมันเป็นคุณสมบัติ value
ยอดเยี่ยม! PlayerScoreboardMoveEditorViewModel
เป็นไดนามิกในขณะนี้
ลองใช้มันและไปที่มุมมองที่จะรับฟังการเปลี่ยนแปลงเหล่านี้จริงๆ
เปิด PlayerScoreboardMoveEditorView
และวิธีการ fillUI
(คุณควรเห็นข้อผิดพลาดในการสร้างในวิธีนี้ ณ จุดนี้เนื่องจากคุณพยายามกำหนดค่า String
ให้กับประเภทวัตถุ Dynamic
)
แทนที่บรรทัด "ผิดพลาด":
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
ด้วยสิ่งต่อไปนี้:
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 }
ถัดไป ใช้ห้าวิธีที่แสดงถึงการดำเนินการย้าย ( ส่วน การดำเนินการของปุ่ม ):
@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() }
เรียกใช้แอพและคลิกที่ปุ่มย้ายบางปุ่ม คุณจะเห็นว่าค่าตัวนับในมุมมองของผู้เล่นเปลี่ยนไปอย่างไรเมื่อคุณคลิกที่ปุ่มการกระทำ
คุณเสร็จสิ้นด้วย PlayerScoreboardMoveEditorView
และ PlayerScoreboardMoveEditorViewModel
นี้เป็นเรื่องง่าย
ตอนนี้ คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ ( GameScoreboardEditorViewController
)
ขั้นแรก เปิด GameScoreboardEditorViewModel
และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของมุมมอง
แทนที่นิยาม time
, score
, isFinished
, isPaused
ด้วยเวอร์ชัน 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 } }
ไปที่การใช้งาน ViewModel ( GameScoreboardEditorViewModelFromGame
) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศไว้ในโปรโตคอล
แทนที่สิ่งนี้:
var time: String var score: String var isFinished: Bool var isPaused: Bool
ด้วยสิ่งต่อไปนี้:
let time: Dynamic<String> let score: Dynamic<String> let isFinished: Dynamic<Bool> let isPaused: Dynamic<Bool>
คุณจะได้รับข้อผิดพลาดเล็กน้อย เนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String
และ Bool
เป็น Dynamic<String>
และ Dynamic<Bool>
มาแก้ไขกันเถอะ
แก้ไขวิธี togglePause
โดยแทนที่ด้วยวิธีต่อไปนี้:
func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }
สังเกตว่าการเปลี่ยนแปลงเพียงอย่างเดียวคือคุณไม่ได้ตั้งค่าคุณสมบัติโดยตรงบนคุณสมบัติอีกต่อไป แต่คุณตั้ง value
คุณสมบัติค่าของวัตถุแทน
ตอนนี้ แก้ไขวิธีการ initWithGame
โดยแทนที่:
self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true
ด้วยสิ่งต่อไปนี้:
self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)
คุณควรได้รับประเด็นในขณะนี้
คุณกำลังรวมค่าดั้งเดิม เช่น String
, Int
และ Bool
ด้วยอ็อบเจ็กต์เวอร์ชัน Dynamic<T>
ซึ่งให้กลไกการโยงแบบเบาแก่คุณ
คุณมีข้อผิดพลาดอีก 1 รายการที่ต้องแก้ไข
ในเมธอด startTimer
ให้แทนที่บรรทัดข้อผิดพลาดด้วย:
self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)
คุณได้อัปเกรด ViewModel ของคุณให้เป็นไดนามิก เช่นเดียวกับที่คุณทำกับ ViewModel ของผู้เล่น แต่คุณยังต้องอัปเดตมุมมองของคุณ ( GameScoreboardEditorViewController
)
แทนที่วิธีการ fillUI
ทั้งหมดด้วยสิ่งนี้:
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] }
ข้อแตกต่างเพียงอย่างเดียวคือคุณเปลี่ยนคุณสมบัติไดนามิกสี่รายการและเพิ่มตัวฟังการเปลี่ยนแปลงให้กับคุณสมบัติแต่ละรายการ
ณ จุดนี้ หากคุณเรียกใช้แอป การสลับปุ่ม เริ่ม/หยุดชั่วคราว จะเริ่มต้นและหยุดตัวจับเวลาเกมชั่วคราว ใช้สำหรับการหมดเวลาระหว่างเกม
คุณใกล้จะเสร็จแล้ว ยกเว้นคะแนนจะไม่เปลี่ยนแปลงใน UI เมื่อคุณกดปุ่มจุดใดปุ่มหนึ่ง (ปุ่ม 1
และ 2
คะแนน)
เนื่องจากคุณยังไม่ได้เผยแพร่การเปลี่ยนแปลงคะแนนจริงในวัตถุโมเดล Game
ที่อ้างอิงถึง ViewModel
ดังนั้นเปิด Game
model object เพื่อตรวจสอบเล็กน้อย ตรวจสอบวิธีการ updateScore
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) }
วิธีนี้ทำสองสิ่งที่สำคัญ
ขั้นแรก จะตั้งค่าคุณสมบัติ isFinished
ให้ true
หากเกมจบลงตามคะแนนของทั้งสองทีม
หลังจากนั้นก็โพสต์แจ้งเตือนว่าคะแนนมีการเปลี่ยนแปลง คุณจะฟังการแจ้งเตือนนี้ใน GameScoreboardEditorViewModelFromGame
และอัปเดตค่าคะแนนแบบไดนามิกในวิธีจัดการการแจ้งเตือน
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.
อะไรตอนนี้?
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
.