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

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text =

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text =

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text =

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM }

จากนั้นใช้ห้าวิธีที่แสดงถึงการเคลื่อนไหว ( การทำงานของปุ่ม มาตรา):

@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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 =

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text =

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM } viewModel.isFinished.bindAndFire { [unowned self] in if

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM { 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 =

Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM



คุณกำลังเริ่มโปรเจ็กต์ iOS ใหม่ซึ่งคุณได้รับจากนักออกแบบทั้งหมดที่จำเป็น .pdf และ .sketch เอกสารและคุณมีวิสัยทัศน์แล้วว่าคุณจะสร้างแอปใหม่นี้อย่างไร

คุณเริ่มถ่ายโอนหน้าจอ UI จากภาพร่างของนักออกแบบไปยัง ViewController ของคุณ .swift, .xib และ .storyboard ไฟล์.



UITextField ที่นี่, UITableView มีอีกสองสาม UILabels และหยิกของ UIButtons. IBOutlets และ IBActions รวมอยู่ด้วย ดีทั้งหมดเรายังอยู่ในโซน UI



อย่างไรก็ตามถึงเวลาทำอะไรกับองค์ประกอบ UI เหล่านี้แล้ว UIButtons จะได้รับนิ้วสัมผัส, UILabels และ UITableViews จะต้องมีคนบอกว่าจะแสดงอะไรและอยู่ในรูปแบบใด



ทันใดนั้นคุณมีโค้ดมากกว่า 3,000 บรรทัด

รหัส Swift 3,000 บรรทัด



คุณจบลงด้วยรหัสสปาเก็ตตี้จำนวนมาก

ขั้นตอนแรกในการแก้ไขปัญหานี้คือการใช้ไฟล์ โมเดล - ดู - คอนโทรลเลอร์ รูปแบบการออกแบบ (MVC) อย่างไรก็ตามรูปแบบนี้มีปัญหาในตัวเอง มี โมเดล - ดู - ดูโมเดล (MVVM) รูปแบบการออกแบบที่ช่วยประหยัดทั้งวัน



การจัดการกับ Spaghetti Code

ในเวลาไม่นานการเริ่มต้นของคุณ ViewController ฉลาดเกินไปและใหญ่เกินไป

รหัสเครือข่ายรหัสแยกวิเคราะห์ข้อมูลรหัสการปรับเปลี่ยนข้อมูลสำหรับการนำเสนอ UI การแจ้งเตือนสถานะแอปการเปลี่ยนแปลงสถานะ UI รหัสทั้งหมดถูกขังอยู่ภายใน if - วิทยาของไฟล์เดียวที่ไม่สามารถใช้ซ้ำได้และจะพอดีกับโปรเจ็กต์นี้เท่านั้น



ของคุณ ViewController รหัสกลายเป็นรหัสสปาเก็ตตี้ที่น่าอับอาย

มันเกิดขึ้นได้อย่างไร?



สาเหตุน่าจะเป็นดังนี้:

คุณรีบเร่งเพื่อดูว่าข้อมูลส่วนหลังทำงานอย่างไรภายใน UITableView ดังนั้นคุณจึงใส่รหัสเครือข่ายสองสามบรรทัดไว้ใน a อุณหภูมิ วิธีการของ ViewController เพียงเพื่อดึงข้อมูลนั้น .json จากเครือข่าย จากนั้นคุณต้องประมวลผลข้อมูลข้างใน .json ดังนั้นคุณจึงเขียนอีกอย่างหนึ่ง อุณหภูมิ วิธีการที่จะทำให้สำเร็จ หรือที่แย่กว่านั้นคือคุณทำด้วยวิธีเดียวกัน



ViewController เติบโตขึ้นเรื่อย ๆ เมื่อมีรหัสการให้สิทธิ์ผู้ใช้เข้ามา จากนั้นรูปแบบข้อมูลก็เริ่มเปลี่ยนไป UI พัฒนาขึ้นและต้องการการเปลี่ยนแปลงที่รุนแรงและคุณก็เพิ่ม if s เข้าไปใน if -ology ที่มีขนาดใหญ่อยู่แล้ว

แต่ทำไม UIViewController สิ่งที่ได้รับจากมือ?

UIViewController เป็นจุดเริ่มต้นที่สมเหตุสมผลในการเริ่มทำงานกับโค้ด UI ของคุณ แสดงถึงหน้าจอจริงที่คุณเห็นขณะใช้แอปใด ๆ กับอุปกรณ์ iOS ของคุณ แม้แต่ Apple ก็ใช้ UIViewControllers ในแอประบบหลักเมื่อสลับไปมาระหว่างแอปต่างๆและ UI แบบเคลื่อนไหว

Apple ใช้สิ่งที่เป็นนามธรรมของ UI ภายใน UIViewController เนื่องจากเป็นส่วนสำคัญของรหัส iOS UI และเป็นส่วนหนึ่งของ MVC รูปแบบการออกแบบ.

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

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

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

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

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

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

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

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

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

MVVM เพื่อช่วยเหลือ

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

ตั้งแต่ UIViewController ควรจะเป็นไฟล์ ตัวควบคุม ในรูปแบบ MVC และมันก็มีประโยชน์มากมายกับไฟล์ มุมมอง เราสามารถรวมเข้ากับไฟล์ ดู รูปแบบใหม่ของเรา - MVVM .

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

ในรูปแบบการออกแบบ MVVM รุ่น เหมือนกับในรูปแบบ MVC แสดงถึงข้อมูลง่ายๆ

ดู แสดงโดย UIView หรือ UIViewController วัตถุพร้อมด้วย .xib และ .storyboard ไฟล์ซึ่งควรแสดงเฉพาะข้อมูลที่เตรียมไว้ (เราไม่ต้องการมี NSDateFormatter โค้ดตัวอย่างเช่นใน View)

เฉพาะสตริงรูปแบบธรรมดาที่มาจากไฟล์ ViewModel .

ViewModel ซ่อนรหัสเครือข่ายแบบอะซิงโครนัสรหัสการเตรียมข้อมูลสำหรับการนำเสนอภาพและการฟังรหัสสำหรับ รุ่น การเปลี่ยนแปลง ทั้งหมดนี้ซ่อนอยู่หลัง API ที่กำหนดไว้อย่างดีซึ่งจำลองมาเพื่อให้เหมาะกับสิ่งนี้โดยเฉพาะ ดู .

ประโยชน์อย่างหนึ่งของการใช้ MVVM คือการทดสอบ ตั้งแต่ ViewModel บริสุทธิ์ NSObject (หรือ struct เป็นต้น) และไม่ได้อยู่คู่กับ UIKit คุณสามารถทดสอบได้ง่ายขึ้นในการทดสอบหน่วยของคุณโดยไม่ส่งผลต่อรหัส UI

ตอนนี้ ดู (UIViewController / UIView) กลายเป็นเรื่องง่ายขึ้นมากในขณะที่ ViewModel ทำหน้าที่เป็นกาวระหว่างไฟล์ รุ่น และ ดู .

การใช้ MVVM ใน Swift

MVVM ใน Swift

หากต้องการแสดงการทำงานของ MVVM คุณสามารถดาวน์โหลดและตรวจสอบตัวอย่างโครงการ Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้ ที่นี่ . โครงการนี้ใช้ Swift 3 และ Xcode 8.1

โครงการมีสองเวอร์ชัน: เริ่มต้น และ เสร็จแล้ว .

เสร็จแล้ว เวอร์ชันเป็นแอปพลิเคชันขนาดเล็กที่สมบูรณ์โดยที่ เริ่มต้น เป็นโครงการเดียวกัน แต่ไม่มีวิธีการและวัตถุที่นำมาใช้

ก่อนอื่นฉันขอแนะนำให้คุณดาวน์โหลดไฟล์ เริ่มต้น โครงการและทำตามบทช่วยสอนนี้ หากคุณต้องการข้อมูลอ้างอิงอย่างรวดเร็วของโครงการในภายหลังให้ดาวน์โหลดไฟล์ เสร็จแล้ว โครงการ.

บทแนะนำโครงการสอน

โครงการสอนเป็นแอปพลิเคชันบาสเก็ตบอลสำหรับติดตามการกระทำของผู้เล่นระหว่างเกม

แอปพลิเคชั่นบาสเก็ตบอล

ใช้สำหรับการติดตามการเคลื่อนไหวของผู้ใช้อย่างรวดเร็วและคะแนนรวมในเกมรับ

สองทีมเล่นจนได้คะแนน 15 (โดยมีคะแนนต่างกันอย่างน้อย 2 คะแนน) ผู้เล่นแต่ละคนสามารถทำคะแนนได้หนึ่งแต้มถึงสองแต้มและผู้เล่นแต่ละคนสามารถช่วยเหลือรีบาวน์และฟาล์วได้

ลำดับชั้นของโครงการมีลักษณะดังนี้:

ลำดับชั้นของโครงการ

รุ่น

ดู

ViewModel

โครงการ Xcode ที่ดาวน์โหลดมามีตัวยึดสำหรับไฟล์ ดู วัตถุ (UIView และ UIViewController) โครงการนี้ยังมีออบเจ็กต์ที่สร้างขึ้นเองบางส่วนที่สร้างขึ้นเพื่อสาธิตวิธีการหนึ่งในการให้ข้อมูลกับไฟล์ ViewModel วัตถุ (Services กลุ่ม)

Extensions กลุ่มมีส่วนขยายที่เป็นประโยชน์สำหรับโค้ด UI ที่ไม่อยู่ในขอบเขตของบทช่วยสอนนี้และอธิบายได้ด้วยตนเอง

หากคุณเรียกใช้แอป ณ จุดนี้แอปจะแสดง UI ที่เสร็จสิ้น แต่ไม่มีอะไรเกิดขึ้นเมื่อผู้ใช้กดปุ่ม

เนื่องจากคุณได้สร้างเฉพาะการดูและ IBActions โดยไม่ต้องเชื่อมต่อกับตรรกะของแอปและไม่ต้องเติมองค์ประกอบ UI ด้วยข้อมูลจากโมเดล (จากอ็อบเจ็กต์ Game ดังที่เราจะเรียนรู้ในภายหลัง)

การเชื่อมต่อ View และ Model กับ ViewModel

ในรูปแบบการออกแบบ MVVM View ไม่ควรรู้อะไรเกี่ยวกับโมเดล สิ่งเดียวที่ 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' 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 ซึ่งเป็น Model ที่อยู่ใต้ 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

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

ตอนนี้มันกำลังแสดงค่าจากออบเจ็กต์ ViewModel ซึ่งรับข้อมูลจากออบเจ็กต์ Model จริง (Game อ็อบเจกต์)

ยอดเยี่ยม! แต่มุมมองของผู้เล่นล่ะ? ปุ่มเหล่านั้นยังคงไม่ทำอะไร

คุณรู้ว่าคุณมีมุมมอง 6 มุมมองสำหรับการติดตามการเคลื่อนไหวของผู้เล่น

คุณสร้างมุมมองย่อยแยกต่างหากชื่อ 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

ณ จุดนี้คุณมีแอปที่ใช้งานได้ซึ่งใช้รูปแบบการออกแบบ MVVM

มันซ่อนโมเดลไว้อย่างดีจากมุมมองและมุมมองของคุณก็ง่ายกว่าที่คุณคุ้นเคยกับ MVC มาก

ถึงจุดนี้คุณได้สร้างแอปที่มี View และ ViewModel

มุมมองนั้นยังมีหกอินสแตนซ์ของมุมมองย่อยเดียวกัน (มุมมองผู้เล่น) พร้อมด้วย ViewModel

อย่างไรก็ตามดังที่คุณสังเกตเห็นคุณสามารถแสดงข้อมูลใน UI ได้เพียงครั้งเดียว (ในวิธี fillUI) และข้อมูลนั้นเป็นแบบคงที่

หากข้อมูลของคุณในมุมมองนั้นไม่เปลี่ยนแปลงตลอดอายุการใช้งานของข้อมูลพร็อพเพอร์ตี้นั้นแสดงว่าคุณมีทางออกที่ดีและสะอาดในการใช้ MVVM ด้วยวิธีนี้

การสร้าง ViewModel Dynamic

เนื่องจากข้อมูลของคุณจะเปลี่ยนแปลงคุณจึงต้องทำให้ ViewModel ของคุณเป็นแบบไดนามิก

สิ่งนี้หมายความว่าเมื่อ Model เปลี่ยนแปลง ViewModel ควรเปลี่ยนค่าคุณสมบัติสาธารณะ มันจะเผยแพร่การเปลี่ยนแปลงกลับไปที่มุมมองซึ่งเป็นสิ่งที่จะอัปเดต UI

มีหลายวิธีในการทำเช่นนี้

เมื่อโมเดลเปลี่ยนแปลง ViewModel จะได้รับการแจ้งเตือนก่อน

คุณต้องมีกลไกบางอย่างเพื่อเผยแพร่สิ่งที่เปลี่ยนแปลงไปยัง View

บางตัวเลือก ได้แก่ RxSwift ซึ่งเป็นห้องสมุดขนาดใหญ่และต้องใช้เวลาพอสมควรในการทำความคุ้นเคย

ViewModel อาจเริ่มทำงาน NSNotification s ในการเปลี่ยนแปลงค่าคุณสมบัติแต่ละครั้ง แต่จะเพิ่มโค้ดจำนวนมากที่ต้องการการจัดการเพิ่มเติมเช่นการสมัครรับการแจ้งเตือนและการยกเลิกการสมัครเมื่อมุมมองถูกยกเลิกการจัดสรร

การสังเกตคีย์ - ค่า (KVO) เป็นอีกทางเลือกหนึ่ง แต่ผู้ใช้จะยืนยันว่า API ของตนนั้นไม่หรูหรา

ในบทช่วยสอนนี้คุณจะใช้ชื่อสามัญและการปิดของ Swift ซึ่งอธิบายไว้อย่างดีใน บทความ Bindings, Generics, Swift และ MVVM .

ตอนนี้กลับไปที่แอปตัวอย่าง

ไปที่กลุ่มโครงการ ViewModel และสร้างไฟล์ Swift ใหม่ Dynamic.swift

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

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

ขั้นแรกเริ่มต้นด้วย PlayerScoreboardMoveEditorView และ ViewModel, PlayerScoreboardMoveEditorViewModel

เปิด PlayerScoreboardMoveEditorViewModel และดูคุณสมบัติของมัน

เพราะ playerName ไม่คาดว่าจะเปลี่ยนแปลงคุณสามารถปล่อยให้เป็นอยู่ได้

คุณสมบัติอีกห้าอย่าง (ห้าประเภทการเคลื่อนไหว) จะเปลี่ยนไปดังนั้นคุณต้องทำอะไรบางอย่างเกี่ยวกับสิ่งนั้น การแก้ไขปัญหา? ดังกล่าวข้างต้น Dynamic คลาสที่คุณเพิ่งเพิ่มลงในโปรเจ็กต์

ข้างใน PlayerScoreboardMoveEditorViewModel ลบคำจำกัดความสำหรับห้าสตริงที่แสดงถึงจำนวนการย้ายและแทนที่ด้วยสิ่งนี้:

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

นี่คือลักษณะของโปรโตคอล ViewModel ตอนนี้:

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

นี้ Dynamic type ช่วยให้คุณสามารถเปลี่ยนค่าของคุณสมบัตินั้น ๆ ได้และในขณะเดียวกันก็แจ้งออบเจ็กต์ change-listener ซึ่งในกรณีนี้จะเป็น View

ตอนนี้อัปเดตการใช้งาน ViewModel จริง PlayerScoreboardMoveEditorViewModelFromPlayer

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

หมายเหตุ: สามารถประกาศคุณสมบัติเหล่านี้เป็นค่าคงที่ด้วย 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

คุณใช้ PlayerScoreboardMoveEditorView เสร็จแล้ว และ PlayerScoreboardMoveEditorViewModel.

นี่เป็นเรื่องง่าย

ตอนนี้คุณต้องทำเช่นเดียวกันกับมุมมองหลักของคุณ (GameScoreboardEditorViewController)

ขั้นแรกให้เปิด GameScoreboardEditorViewModel และดูว่าค่าใดที่คาดว่าจะเปลี่ยนแปลงระหว่างวงจรชีวิตของข้อมูลพร็อพเพอร์ตี้

แทนที่ time, score, isFinished, isPaused คำจำกัดความด้วย Dynamic เวอร์ชัน:

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

ไปที่การใช้งาน ViewModel (GameScoreboardEditorViewModelFromGame) และทำเช่นเดียวกันกับคุณสมบัติที่ประกาศในโปรโตคอล

แทนที่สิ่งนี้:

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

ดังต่อไปนี้:

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

ตอนนี้คุณจะได้รับข้อผิดพลาดเล็กน้อยเนื่องจากคุณเปลี่ยนประเภทของ ViewModel จาก String และ Bool ถึง Dynamic และ Dynamic.

มาแก้ไขกัน

แก้ไข 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 เวอร์ชันของวัตถุเหล่านั้นซึ่งให้กลไกการเข้าเล่มแบบน้ำหนักเบา

คุณมีข้อผิดพลาดอีกอย่างที่ต้องแก้ไข

ใน 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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM ? '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 model object จนถึง ViewModel

ดังนั้นเปิด Game โมเดลวัตถุสำหรับการตรวจสอบเล็กน้อย ตรวจสอบ 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 และอัปเดตค่าคะแนนแบบไดนามิกในวิธีการจัดการการแจ้งเตือน

เพิ่มบรรทัดนี้ที่ด้านล่างของ initWithGame วิธีการ (อย่าลืม super.init() โทรเพื่อหลีกเลี่ยงข้อผิดพลาด):

super.init() subscribeToNotifications()

ด้านล่าง initWithGame วิธีการเพิ่ม deinit เนื่องจากคุณต้องการล้างข้อมูลอย่างถูกต้องและหลีกเลี่ยงข้อขัดข้องที่เกิดจาก NotificationCenter

deinit { unsubscribeFromNotifications() }

สุดท้ายเพิ่มการใช้งานของวิธีการเหล่านี้ เพิ่มส่วนนี้ด้านล่าง deinit วิธี:

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

ตอนนี้เรียกใช้แอพและคลิกที่มุมมองของผู้เล่นเพื่อเปลี่ยนคะแนน เนื่องจากคุณได้เชื่อมต่อไดนามิก score แล้ว และ isFinished ใน ViewModel ที่มี View ทุกอย่างจะทำงานเมื่อคุณเปลี่ยนค่าคะแนนภายใน ViewModel

วิธีการปรับปรุงแอปเพิ่มเติม

แม้ว่าจะมีช่องว่างสำหรับการปรับปรุงอยู่เสมอ แต่ก็อยู่นอกขอบเขตของบทแนะนำนี้

ตัวอย่างเช่นเราไม่หยุดเวลาโดยอัตโนมัติเมื่อเกมจบลง (เมื่อทีมใดทีมหนึ่งถึง 15 คะแนน) เราเพียงแค่ซ่อนมุมมองของผู้เล่น

คุณสามารถเล่นกับแอปนี้ได้หากต้องการและอัปเกรดให้มีมุมมอง 'ผู้สร้างเกม' ซึ่งจะสร้างเกมกำหนดชื่อทีมกำหนดชื่อผู้เล่นและสร้าง Game วัตถุที่สามารถใช้ในการนำเสนอ GameScoreboardEditorViewController.

เราสามารถสร้างมุมมอง 'รายการเกม' อื่นที่ใช้ UITableView เพื่อแสดงหลายเกมที่กำลังดำเนินอยู่พร้อมข้อมูลโดยละเอียดบางอย่างในเซลล์ตาราง ในการเลือกเซลล์เราสามารถแสดง GameScoreboardEditorViewController กับ Game ที่เลือก

GameLibrary ได้รับการดำเนินการแล้ว อย่าลืมส่งการอ้างอิงไลบรารีนั้นไปยังอ็อบเจ็กต์ ViewModel ใน initializer ตัวอย่างเช่น ViewModel ของ“ ผู้สร้างเกม” จะต้องมีอินสแตนซ์ของ GameLibrary ผ่านตัวเริ่มต้นเพื่อให้สามารถแทรก Game ที่สร้างขึ้น วัตถุลงในไลบรารี ViewModel 'ของรายการเกม' ยังต้องการข้อมูลอ้างอิงนี้เพื่อดึงข้อมูลเกมทั้งหมดจากไลบรารีซึ่ง UITableView จะต้องการ

แนวคิดคือการซ่อนงานสกปรก (ที่ไม่ใช่ UI) ทั้งหมดภายใน ViewModel และให้ UI (View) ดำเนินการกับข้อมูลการนำเสนอที่เตรียมไว้เท่านั้น

อะไรตอนนี้?

หลังจากคุณคุ้นเคยกับ MVVM แล้วคุณสามารถปรับปรุงเพิ่มเติมได้โดยใช้ กฎของ Uncle Bob’s Clean Architecture .

การอ่านที่ดีเพิ่มเติมคือบทแนะนำสามส่วนเกี่ยวกับสถาปัตยกรรม Android:

ตัวอย่างเขียนด้วย Java (สำหรับ Android) และหากคุณคุ้นเคยกับ Java (ซึ่งใกล้เคียงกับ Swift มากขึ้นดังนั้น Objective-C ก็คือ Java) คุณจะได้รับแนวคิดเกี่ยวกับวิธีการ refactor โค้ดของคุณเพิ่มเติมภายในออบเจ็กต์ ViewModel พวกเขาไม่นำเข้าโมดูล iOS ใด ๆ (UIKit หรือ CoreLocation เช่น)

โมดูล iOS เหล่านี้สามารถซ่อนอยู่หลัง NSObjects ที่บริสุทธิ์ซึ่งดีสำหรับการใช้โค้ดซ้ำ

MVVM เป็นทางเลือกที่ดีสำหรับคนส่วนใหญ่ iOS และหวังว่าคุณจะลองใช้งานในโครงการถัดไป หรือลองใช้ในโปรเจ็กต์ปัจจุบันของคุณเมื่อคุณสร้าง UIViewController

ที่เกี่ยวข้อง: การทำงานกับรูปแบบคงที่: บทช่วยสอน Swift MVVM