บทช่วยสอน 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 บรรทัด

รหัส Swift 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

ที่เกี่ยวข้อง: 10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา iOS ไม่รู้ว่าพวกเขากำลังทำอยู่

การอัพเกรดเป็นรูปแบบการออกแบบ MVC

รูปแบบการออกแบบ MVC

ในรูปแบบการออกแบบ MVC มุมมอง ควรจะปิดใช้งานและแสดงเฉพาะข้อมูลที่เตรียมไว้ตามต้องการ

Controller ควรทำงานกับข้อมูล Model เพื่อเตรียมข้อมูลสำหรับ Views ซึ่งจะแสดงข้อมูลนั้น

มุมมอง ยังรับผิดชอบในการแจ้งผู้ ควบคุม เกี่ยวกับการดำเนินการใดๆ เช่น การสัมผัสของผู้ใช้

ดังที่ได้กล่าวมาแล้ว UIViewController มักจะเป็นจุดเริ่มต้นในการสร้างหน้าจอ UI สังเกตว่าในชื่อของมัน มีทั้ง "มุมมอง" และ "ตัวควบคุม" ซึ่งหมายความว่า "ควบคุมมุมมอง" ไม่ได้หมายความว่าทั้งโค้ด "คอนโทรลเลอร์" และ "มุมมอง" ควรเข้าไปข้างใน

การผสมผสานของรหัสมุมมองและตัวควบคุมนี้มักเกิดขึ้นเมื่อคุณย้าย IBOutlets ของมุมมองย่อยเล็กๆ ภายใน UIViewController และจัดการการดูย่อยเหล่านั้นโดยตรงจาก UIViewController คุณควรรวมโค้ดนั้นไว้ในคลาสย่อย UIView ที่กำหนดเองแทน

สังเกตได้ง่ายว่าสิ่งนี้อาจนำไปสู่การข้ามเส้นทางของรหัสดูและตัวควบคุม

MVVM สู่การช่วยเหลือ

นี่คือจุดที่รูปแบบ MVVM มีประโยชน์

เนื่องจาก UIViewController ควรจะเป็น Controller ในรูปแบบ MVC และได้ดำเนินการหลายอย่างกับ Views แล้ว เราจึงสามารถรวมมันเข้ากับ View ของรูปแบบใหม่ของเรา - MVVM

รูปแบบการออกแบบ 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 ใน 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

ตอนนี้ เรียกใช้แอป ควรมีลักษณะดังนี้:

iOS App

มุมมองตรงกลาง ซึ่งรับผิดชอบคะแนน เวลา และชื่อทีม จะไม่แสดงค่าที่ตั้งไว้ในตัวสร้างอินเทอร์เฟซอีกต่อไป

ตอนนี้มันกำลังแสดงค่าจากวัตถุ 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 อย่างไร

iOS App

ณ จุดนี้ คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ 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() }

เรียกใช้แอพและคลิกที่ปุ่มย้ายบางปุ่ม คุณจะเห็นว่าค่าตัวนับในมุมมองของผู้เล่นเปลี่ยนไปอย่างไรเมื่อคุณคลิกที่ปุ่มการกระทำ

iOS App

คุณเสร็จสิ้นด้วย 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 .

Related: Working With Static Patterns: A Swift MVVM Tutorial