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

เผยแพร่แล้ว: 2022-03-11

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

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

มาดูกันดีกว่าว่ารูปแบบการออกแบบ Swift สองรูปแบบนั้นทำงานได้ดีเพียงใดกับแอปพลิเคชั่นแชทที่ทันสมัย: รูปแบบ classic model-view-controller (MVC) และ model-view-viewmodel pattern ที่ไม่เปลี่ยนรูปอย่างง่าย (MVVM ซึ่งบางครั้งทำให้มีสไตล์ “รูปแบบ ViewModel” ”). แอปแชทเป็นตัวอย่างที่ดีเพราะมีแหล่งข้อมูลมากมายและจำเป็นต้องอัปเดต UI ด้วยวิธีต่างๆ มากมายทุกครั้งที่ได้รับข้อมูล

แอปพลิเคชั่นแชทของเรา

แอปพลิเคชันที่เราจะใช้เป็นแนวทางในบทช่วยสอน Swift MVVM นี้จะมีคุณสมบัติพื้นฐานส่วนใหญ่ที่เรารู้จักจากแอปพลิเคชันแชท เช่น WhatsApp มาดูคุณสมบัติที่เราจะนำไปใช้และเปรียบเทียบ MVVM กับ MVC แอปพลิเคชัน:

  • จะโหลดการแชทที่ได้รับก่อนหน้านี้จากดิสก์
  • จะซิงค์การแชทที่มีอยู่ผ่าน GET -request กับเซิร์ฟเวอร์
  • จะได้รับการแจ้งเตือนแบบพุชเมื่อมีการส่งข้อความใหม่ถึงผู้ใช้
  • จะเชื่อมต่อกับ WebSocket เมื่อเราอยู่ในหน้าจอแชท
  • สามารถ POST สต์ข้อความใหม่ในการแชท
  • จะแสดงการแจ้งเตือนในแอปเมื่อได้รับข้อความใหม่จากการแชทที่เราไม่ได้อยู่ในขณะนี้
  • จะแสดงข้อความใหม่ทันทีเมื่อเราได้รับข้อความใหม่สำหรับแชทปัจจุบัน
  • จะส่งข้อความที่อ่านแล้วเมื่อเราอ่านข้อความที่ยังไม่ได้อ่าน
  • จะได้รับข้อความที่อ่านแล้วเมื่อมีคนอ่านข้อความของเรา
  • อัปเดตป้ายตัวนับข้อความที่ยังไม่ได้อ่านบนไอคอนแอปพลิเคชัน
  • ซิงค์ข้อความทั้งหมดที่ได้รับหรือเปลี่ยนกลับเป็น Core Data

ในแอปพลิเคชันสาธิตนี้ จะไม่มีการนำ API, WebSocket หรือ Core Data ไปใช้จริง เพื่อให้การนำโมเดลไปใช้ง่ายขึ้นอีกเล็กน้อย ฉันได้เพิ่มแชทบอทที่จะเริ่มตอบกลับเมื่อคุณเริ่มการสนทนา อย่างไรก็ตาม การกำหนดเส้นทางและการโทรอื่น ๆ ทั้งหมดจะถูกนำไปใช้งานเช่นเดียวกับที่เคยเป็นหากการจัดเก็บและการเชื่อมต่อเป็นจริง รวมถึงการหยุดชั่วคราวแบบอะซิงโครนัสเล็กน้อยก่อนที่จะกลับมา

มีการสร้างสามหน้าจอต่อไปนี้:

หน้าจอรายการแชท สร้างแชท และหน้าจอข้อความ

MVC คลาสสิก

อย่างแรกเลย มีรูปแบบ MVC มาตรฐานสำหรับการสร้างแอปพลิเคชัน iOS นี่คือวิธีที่ Apple จัดโครงสร้างโค้ดเอกสารทั้งหมดและวิธีที่ API และองค์ประกอบ UI คาดหวังให้ทำงาน คนส่วนใหญ่ได้รับการสอนเมื่อลงเรียนหลักสูตร iOS

บ่อยครั้งที่ MVC ถูกตำหนิว่านำไปสู่การป่อง UIViewController ของโค้ดสองสามพันบรรทัด แต่ถ้าใช้ได้ดี ด้วยการแยกชั้นที่ดีระหว่างแต่ละเลเยอร์ เราสามารถมี ViewController ที่ค่อนข้างบางซึ่งทำหน้าที่เหมือนตัวจัดการระดับกลางระหว่าง View s, Model s และ Controller อื่นๆ เท่านั้น

นี่คือผังงานสำหรับการนำ MVC ไปใช้ของแอป ( CreateViewController เพื่อความชัดเจน):

ผังงานการนำ MVC ไปใช้ โดยละเว้น CreateViewController เพื่อความชัดเจน

มาดูชั้นในรายละเอียดกัน

แบบอย่าง

เลเยอร์โมเดลมักจะเป็นเลเยอร์ที่มีปัญหาน้อยที่สุดใน MVC ในกรณีนี้ ฉันเลือกใช้ ChatWebSocket , ChatModel และ PushNotificationController เพื่อเป็นตัวกลางระหว่างออบเจ็กต์ Chat และ Message แหล่งข้อมูลภายนอก และส่วนที่เหลือของแอปพลิเคชัน ChatModel เป็นแหล่งที่มาของความจริงภายในแอปพลิเคชันและใช้งานได้เฉพาะในหน่วยความจำในแอปพลิเคชันสาธิตนี้ ในแอปพลิเคชันในชีวิตจริง มันอาจจะได้รับการสนับสนุนจาก Core Data สุดท้าย ChatEndpoint จะจัดการการโทร HTTP ทั้งหมด

ดู

มุมมองค่อนข้างใหญ่เนื่องจากต้องจัดการกับความรับผิดชอบมากมาย เนื่องจากฉันได้แยกโค้ดการดูทั้งหมดออกจาก UIViewController อย่างระมัดระวัง ฉันได้ทำสิ่งต่อไปนี้:

  • ใช้รูปแบบ enum สถานะ (แนะนำได้มาก) เพื่อกำหนดสถานะที่มุมมองอยู่ในขณะนี้
  • เพิ่มฟังก์ชันที่เชื่อมต่อกับปุ่มและรายการอินเทอร์เฟซที่ทริกเกอร์การดำเนินการอื่น ๆ (เช่นการแตะ Return ขณะป้อนชื่อผู้ติดต่อ)
  • ตั้งค่าข้อจำกัดและโทรกลับไปยังผู้รับมอบสิทธิ์ทุกครั้ง

เมื่อคุณรวม UITableView เข้าด้วยกัน มุมมองจะมีขนาดใหญ่กว่า UIViewController มาก ซึ่งนำไปสู่โค้ดที่น่าเป็นห่วงมากกว่า 300 บรรทัดและงานที่หลากหลายใน ChatView

คอนโทรลเลอร์

เนื่องจากตรรกะการจัดการแบบจำลองทั้งหมดได้ย้ายไปที่ ChatModel โค้ดการดูทั้งหมด ซึ่งอาจแฝงตัวอยู่ที่นี่ในโปรเจ็กต์ที่แยกจากกันซึ่งเหมาะสมน้อยกว่า ตอนนี้อยู่ในมุมมอง ดังนั้น UIViewController จึงค่อนข้างบาง ตัวควบคุมการดูจะลืมไปเลยว่าข้อมูลโมเดลมีลักษณะอย่างไร ดึงข้อมูลมาอย่างไร หรือควรแสดงอย่างไร—เพียงแค่พิกัดเท่านั้น ในโครงการตัวอย่าง ไม่มี UIViewController ใดที่โค้ดเกิน 150 บรรทัด

อย่างไรก็ตาม ViewController ยังคงทำสิ่งต่อไปนี้:

  • เป็นตัวแทนสำหรับมุมมองและตัวควบคุมมุมมองอื่น ๆ
  • สร้างอินสแตนซ์และกด (หรือเปิด) ดูคอนโทรลเลอร์หากจำเป็น
  • การส่งและรับสายเข้าและออกจาก ChatModel
  • การเริ่มและหยุด WebSocket ขึ้นอยู่กับระยะของวงจรควบคุมมุมมอง
  • การตัดสินใจอย่างมีเหตุผล เช่น ไม่ส่งข้อความหากว่างเปล่า
  • กำลังปรับปรุงมุมมอง

นี่ยังมีอีกมาก แต่ส่วนใหญ่เป็นการประสานงาน การประมวลผลบล็อคการโทรกลับ และการส่งต่อ

ประโยชน์

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

ข้อเสีย

  • ดูคอนโทรลเลอร์มีงานมากมาย ส่วนใหญ่จะส่งข้อมูลไปมาระหว่างมุมมองและเลเยอร์โมเดล
  • ไม่เหมาะที่จะจัดการกับแหล่งที่มาของเหตุการณ์หลายแหล่ง
  • ชั้นเรียนมักจะรู้มากเกี่ยวกับชั้นเรียนอื่น ๆ

คำจำกัดความของปัญหา

วิธีนี้ใช้ได้ผลดีตราบใดที่แอปพลิเคชันปฏิบัติตามการกระทำของผู้ใช้และตอบสนองต่อการกระทำดังกล่าว เช่นเดียวกับที่คุณจินตนาการว่าแอปพลิเคชันอย่าง Adobe Photoshop หรือ Microsoft Word จะใช้งานได้ ผู้ใช้ดำเนินการ อัปเดต UI ทำซ้ำ

แต่แอปพลิเคชั่นที่ทันสมัยเชื่อมต่อกัน มักมีมากกว่าหนึ่งวิธี ตัวอย่างเช่น คุณโต้ตอบผ่าน REST API รับการแจ้งเตือนแบบพุช และในบางกรณี คุณเชื่อมต่อกับ WebSocket ด้วย

ด้วยเหตุนี้ View Controller จึงจำเป็นต้องจัดการกับแหล่งข้อมูลเพิ่มเติม และเมื่อใดก็ตามที่ได้รับข้อความภายนอกโดยที่ผู้ใช้ไม่ได้เรียกข้อความนั้น เช่น รับข้อความผ่าน WebSocket แหล่งข้อมูลจำเป็นต้องค้นหาทางกลับไปทางขวา ดูคอนโทรลเลอร์ สิ่งนี้ต้องการโค้ดจำนวนมากเพียงเพื่อกาวทุกส่วนเข้าด้วยกันเพื่อทำงานที่เหมือนกัน

แหล่งข้อมูลภายนอก

มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราได้รับข้อความพุช:

 class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure("Chat for received message should always exist") } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

เราต้องขุดผ่าน stack of view controller ด้วยตนเองเพื่อดูว่ามี view controller ที่ต้องอัปเดตตัวเองหรือไม่หลังจากที่เราได้รับการแจ้งเตือนแบบพุช ในกรณีนี้ เรายังต้องการอัปเดตหน้าจอที่ใช้ UpdatedChatDelegate ซึ่งในกรณีนี้ เป็นเพียง ChatsViewController เราทำสิ่งนี้ด้วยเพื่อให้รู้ว่าเราควรระงับการแจ้งเตือนหรือไม่เพราะเราดู Chat ที่ตั้งใจไว้อยู่แล้ว ในกรณีนั้น ในที่สุด เราก็ส่งข้อความไปยังตัวควบคุมการดูแทน เห็นได้ชัดว่า PushNotificationController จำเป็นต้องรู้เกี่ยวกับแอปพลิเคชันมากเกินไปจึงจะสามารถทำงานได้

หาก ChatWebSocket จะส่งข้อความไปยังส่วนอื่น ๆ ของแอปพลิเคชันด้วย แทนที่จะมีความสัมพันธ์แบบหนึ่งต่อหนึ่งกับ ChatViewController เราจะประสบปัญหาเดียวกันที่นั่น

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

ผู้แทน

รูปแบบ MVC ยังเพิ่มความซับซ้อนเป็นพิเศษให้กับมิกซ์เมื่อเราเพิ่มตัวควบคุมมุมมองอื่นๆ นี่เป็นเพราะว่าผู้ควบคุมการดูมักจะรู้จักซึ่งกันและกันผ่านตัวแทน ผู้กำหนดค่าเริ่มต้น และ—ในกรณีของกระดานเรื่องราว prepareForSegue เมื่อส่งข้อมูลและข้อมูลอ้างอิง ตัวควบคุมการดูทุกตัวจัดการการเชื่อมต่อของตัวเองกับรุ่นหรือตัวควบคุมสื่อกลาง และทั้งคู่กำลังส่งและรับการอัปเดต

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

เป็นไปได้ที่จะทำลายตัวควบคุมการดูหนึ่งตัวโดยการเปลี่ยนรหัสในอีกตัวหนึ่ง เช่น ข้อมูลเก่าใน ChatsListViewController เนื่องจาก ChatViewController ไม่ได้เรียก updated(chat: Chat) อีกต่อไป โดยเฉพาะอย่างยิ่งในสถานการณ์ที่ซับซ้อนมากขึ้น การรักษาทุกอย่างให้ตรงกันเป็นเรื่องเจ็บปวด

การแยกระหว่าง View และ Model

การลบโค้ดที่เกี่ยวข้องกับการดูทั้งหมดออกจาก view controller ไปยัง customView s และย้ายโค้ดที่เกี่ยวข้องกับโมเดลทั้งหมดไปยังคอนโทรลเลอร์เฉพาะ คอนโทรลเลอร์การดูจะค่อนข้างบางและแยกจากกัน อย่างไรก็ตาม ยังมีปัญหาอยู่หนึ่งข้อ นั่นคือ มีช่องว่างระหว่างสิ่งที่มุมมองต้องการแสดงกับข้อมูลที่อยู่ในแบบจำลอง ตัวอย่างที่ดีคือ ChatListView สิ่งที่เราต้องการแสดงคือรายการเซลล์ที่บอกเราว่าเรากำลังคุยกับใคร ข้อความสุดท้ายคืออะไร วันที่ของข้อความล่าสุด และจำนวนข้อความที่ยังไม่ได้อ่านที่เหลืออยู่ในการ Chat :

ตัวนับข้อความที่ยังไม่ได้อ่านในหน้าจอแชท

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

 class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

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

 var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

ในที่สุด เราก็จัดรูปแบบวันที่ใน ChatItemTableViewCell เมื่อเราแสดง:

 func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? "" lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? "" show(unreadMessageCount: chat.unreadMessages) }

แม้แต่ในตัวอย่างที่ค่อนข้างง่าย ก็ค่อนข้างชัดเจนว่ามีความตึงเครียดระหว่างสิ่งที่มุมมองต้องการกับสิ่งที่ตัวแบบมีให้

MVVM ที่ขับเคลื่อนด้วยเหตุการณ์แบบคงที่ หรือที่เรียกว่า "รูปแบบการดูโมเดล" ที่ขับเคลื่อนด้วยเหตุการณ์แบบคงที่

MVVM แบบคงที่ทำงานกับโมเดลการดู แต่แทนที่จะสร้างการรับส่งข้อมูลแบบสองทิศทางผ่านพวกมัน—เหมือนกับที่เราเคยมีผ่านตัวควบคุมการดูด้วย MVC— เราสร้างโมเดลการดูที่ไม่เปลี่ยนรูปแบบที่อัปเดต UI ทุกครั้งที่ UI จำเป็นต้องเปลี่ยนเพื่อตอบสนองต่อเหตุการณ์ .

โค้ดสามารถทริกเกอร์เหตุการณ์ได้เกือบทุกส่วน ตราบใดที่สามารถให้ข้อมูลที่เกี่ยวข้องซึ่งเหตุการณ์ enum ต้องการ ตัวอย่างเช่น การรับเหตุการณ์ที่ received(new: Message) สามารถทริกเกอร์ได้โดยการแจ้งเตือนแบบพุช WebSocket หรือการโทรผ่านเครือข่ายปกติ

ลองดูในแผนภาพ:

ผังงานการนำ MVVM ไปใช้

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

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

หลังจากปรับโครงสร้างใหม่ ฉันก็ลงเอยด้วยคลาสใหม่มากมายดังที่แสดงไว้ข้างต้น คุณสามารถค้นหาการใช้งานเวอร์ชัน MVVM แบบคงที่ของฉันได้ที่ GitHub อย่างไรก็ตาม เมื่อฉันเปรียบเทียบการเปลี่ยนแปลงกับเครื่องมือ cloc จะเห็นได้ชัดว่าไม่มีโค้ดพิเศษมากนัก:

ลวดลาย ไฟล์ ว่างเปล่า ความคิดเห็น รหัส
MVC 30 386 217 1807
MVVM 51 442 359 1981

โค้ดมีเพิ่มขึ้นเพียง 9 เปอร์เซ็นต์เท่านั้น ที่สำคัญกว่านั้น ขนาดเฉลี่ยของไฟล์เหล่านี้ลดลงจากโค้ด 60 บรรทัดเหลือเพียง 39 บรรทัด

แผนภูมิวงกลมแบบบรรทัดของโค้ด ดูคอนโทรลเลอร์: MVC 287 เทียบกับ MVVM 154 หรือน้อยกว่า 47%; การดู: MVC 523 เทียบกับ MVVM 392 หรือน้อยกว่า 26%

สิ่งที่สำคัญที่สุดคือสามารถพบการลดลงที่ใหญ่ที่สุดได้ในไฟล์ที่โดยทั่วไปแล้วจะใหญ่ที่สุดใน MVC: มุมมองและตัวควบคุมการดู มุมมองมีเพียง 74 เปอร์เซ็นต์ของขนาดดั้งเดิมและตัวควบคุมการดูตอนนี้มีเพียง 53 เปอร์เซ็นต์ของขนาดดั้งเดิม

ควรสังเกตด้วยว่าโค้ดพิเศษจำนวนมากคือโค้ดไลบรารีที่ช่วยแนบบล็อกกับปุ่มและวัตถุอื่นๆ ในแผนผังภาพ โดยไม่ต้องใช้ @IBAction แบบคลาสสิกของ MVC หรือรูปแบบการมอบสิทธิ์

มาสำรวจชั้นต่างๆ ของการออกแบบนี้ทีละชั้นกัน

เหตุการณ์

เหตุการณ์จะเป็น enum เสมอ ซึ่งมักจะมีค่าที่เกี่ยวข้องกัน โดยมากมักจะทับซ้อนกับเอนทิตีตัวใดตัวหนึ่งในแบบจำลองของคุณ แต่ไม่จำเป็นเสมอไป ในกรณีนี้ แอปพลิเคชันจะแบ่งออกเป็นสองเหตุการณ์หลัก enum s: ChatEvent และ MessageEvent ChatEvent ใช้สำหรับอัปเดตทั้งหมดเกี่ยวกับวัตถุแชท:

 enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

อีกอันหนึ่งเกี่ยวข้องกับเหตุการณ์ที่เกี่ยวข้องกับข้อความทั้งหมด:

 enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

สิ่งสำคัญคือการจำกัด *Event enum ของคุณให้มีขนาดที่เหมาะสม หากคุณต้องการ 10 กรณีขึ้นไป นั่นเป็นสัญญาณว่าคุณกำลังพยายามครอบคลุมมากกว่าหนึ่งเรื่อง

หมายเหตุ: แนวคิด enum นั้นทรงพลังอย่างยิ่งใน Swift ฉันมักจะใช้ enum s กับค่าที่เกี่ยวข้องกันเป็นจำนวนมาก เนื่องจากสามารถขจัดความคลุมเครือมากมายที่คุณอาจมีด้วยค่าทางเลือก

บทช่วยสอน Swift MVVM: เราเตอร์เหตุการณ์

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

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

เราเตอร์เหตุการณ์ควรทราบแหล่งที่มาของเหตุการณ์ให้น้อยที่สุดเท่าที่จะเป็นไปได้และไม่ควรให้อะไรเลย ไม่มีเหตุการณ์ใดในแอปพลิเคชันตัวอย่างนี้มีตัวบ่งชี้ว่ามาจากไหน ดังนั้นจึงง่ายมากที่จะผสมในแหล่งข้อความประเภทใดก็ได้ ตัวอย่างเช่น WebSocket ทริกเกอร์เหตุการณ์เดียวกัน received(message: Message, contact: String) — เป็นการแจ้งเตือนแบบพุชใหม่

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

 class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

มีเรื่องเล็กน้อยเกิดขึ้นที่นี่: สิ่งเดียวที่เราทำคืออัปเดตโมเดลและส่งต่อกิจกรรมไปยัง ChatEventHandler เพื่อให้ UI ได้รับการอัปเดต

บทช่วยสอน Swift MVVM: Model Controller

นี่เป็นคลาสเดียวกับที่เราใช้ใน MVC เนื่องจากใช้งานได้ดีอยู่แล้ว มันแสดงถึงสถานะของแอปพลิเคชันและมักจะได้รับการสนับสนุนโดย Core Data หรือไลบรารีการจัดเก็บข้อมูลในเครื่อง

เลเยอร์โมเดล—หากนำไปใช้อย่างถูกต้องใน MVC—แทบไม่จำเป็นต้องมีการปรับโครงสร้างใหม่เพื่อให้พอดีกับรูปแบบต่างๆ การเปลี่ยนแปลงที่ใหญ่ที่สุดคือการเปลี่ยนโมเดลเกิดขึ้นจากคลาสที่น้อยลง ทำให้ชัดเจนขึ้นเล็กน้อยว่าการเปลี่ยนแปลงเกิดขึ้นที่ใด

ในอีกทางหนึ่งกับรูปแบบนี้ คุณสามารถสังเกตการเปลี่ยนแปลงของแบบจำลองและตรวจดูให้แน่ใจว่าได้รับการจัดการ ในกรณีนี้ ฉันเลือกที่จะให้เฉพาะ *EventRouter และ *Endpoint เปลี่ยนแปลงโมเดล ดังนั้นจึงมีความรับผิดชอบที่ชัดเจนเกี่ยวกับตำแหน่งและเวลาที่โมเดลได้รับการอัปเดต ในทางตรงกันข้าม หากเราสังเกตการเปลี่ยนแปลง เราจะต้องเขียนโค้ดเพิ่มเติมเพื่อเผยแพร่เหตุการณ์ที่ไม่เปลี่ยนแปลงโมเดล เช่น ข้อผิดพลาดผ่าน ChatEventHandler ซึ่งจะทำให้ไม่ชัดเจนว่าเหตุการณ์ไหลผ่านแอปพลิเคชันอย่างไร

บทช่วยสอน Swift MVVM: ตัวจัดการเหตุการณ์

ตัวจัดการเหตุการณ์เป็นที่ที่ตัวควบคุมการดูหรือมุมมองสามารถลงทะเบียน (และยกเลิกการลงทะเบียน) ตัวเองเป็นผู้ฟังเพื่อรับโมเดลมุมมองที่อัปเดต ซึ่งสร้างขึ้นเมื่อใดก็ตามที่ ChatEventRouter เรียกใช้ฟังก์ชันบน ChatEventHandler

คุณจะเห็นว่ามันสะท้อนถึงสถานะการดูทั้งหมดที่เราเคยใช้ใน MVC มาก่อนอย่างคร่าวๆ หากคุณต้องการการอัปเดต UI ประเภทอื่นๆ เช่น เสียงหรือการเรียกใช้ Taptic engine สามารถทำได้จากที่นี่เช่นกัน

 protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

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

บทช่วยสอน Swift MVVM: ดูรุ่น

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

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

เป้าหมายหนึ่งของการใช้รูปแบบนี้คือเราไม่มีสถานะในใบสมัครเว้นแต่จำเป็นจริงๆ รัฐคืออะไรกันแน่? โดยพื้นฐานแล้วรัฐเป็นสถานที่ที่เราจัดเก็บการแสดงข้อมูลประเภทใดประเภทหนึ่งโดยเฉพาะ สถานะพิเศษประเภทหนึ่งคือสถานะที่ UI ของคุณอยู่ในปัจจุบัน ซึ่งแน่นอนว่าเราไม่สามารถป้องกันได้ด้วยแอปพลิเคชันที่ขับเคลื่อนด้วย UI สถานะประเภทอื่น ๆ ทั้งหมดเกี่ยวข้องกับข้อมูล หากเรามีสำเนาอาร์เรย์ของ Chat สำรอง UITableView ของเราในหน้าจอรายการแชท นั่นคือตัวอย่างของสถานะที่ซ้ำกัน โมเดลมุมมองสองทางแบบเดิมจะเป็นอีกตัวอย่างหนึ่งของสำเนา Chat ของผู้ใช้ของเรา

โดยการส่งผ่านโมเดลมุมมองที่ไม่เปลี่ยนรูปซึ่งได้รับการรีเฟรชทุกครั้งที่มีการเปลี่ยนแปลงโมเดล เราจะกำจัดสถานะที่ซ้ำกันประเภทนี้ เพราะหลังจากปรับใช้ตัวเองกับ UI แล้ว จะไม่มีการใช้งานอีกต่อไป จากนั้นเรามีสถานะเพียงสองประเภทเท่านั้นที่เราไม่สามารถหลีกเลี่ยงได้—UI และรุ่น—และมีความสอดคล้องกันอย่างสมบูรณ์

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

ดังนั้นมันอาจเป็นโครงสร้างที่ไม่เปลี่ยนรูปแบบ struct เพื่อให้โครงสร้างนี้ struct ง่ายที่สุด เราจะสร้างอินสแตนซ์ด้วยตัวสร้างโมเดลการดู สิ่งที่น่าสนใจอย่างหนึ่งเกี่ยวกับโมเดลการดูคือได้รับแฟล็กพฤติกรรมเช่น shouldShowBusy และ shouldShowError ที่แทนที่กลไก state enum ที่พบในมุมมองก่อนหน้านี้ นี่คือข้อมูลสำหรับ ChatItemTableViewCell ที่เราเคยวิเคราะห์มาก่อน:

 struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

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

ดูตัวสร้างแบบจำลอง

ตัวสร้างโมเดลการดูสามารถสร้างอินสแตนซ์ของโมเดลการดู โดยเปลี่ยนอินพุต เช่น Chat หรือ Message ให้เป็นโมเดลการดูที่ได้รับการปรับแต่งอย่างสมบูรณ์แบบสำหรับมุมมองเฉพาะ สิ่งสำคัญที่สุดอย่างหนึ่งที่เกิดขึ้นในตัวสร้างโมเดลการดูคือการกำหนดว่าเกิดอะไรขึ้นจริงภายในบล็อคในโมเดลการดู บล็อกที่แนบมาโดยตัวสร้างโมเดลมุมมองควรสั้นมาก โดยเรียกใช้ฟังก์ชันของส่วนอื่น ๆ ของสถาปัตยกรรมโดยเร็วที่สุด บล็อกดังกล่าวไม่ควรมีเหตุผลทางธุรกิจใดๆ

 class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? "" let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? "" let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

ตอนนี้การจัดรูปแบบล่วงหน้าทั้งหมดเกิดขึ้นในที่เดียวกัน และพฤติกรรมก็จะถูกตัดสินที่นี่เช่นกัน เป็นคลาสที่สำคัญในลำดับชั้นนี้ และอาจเป็นเรื่องที่น่าสนใจที่จะเห็นว่าผู้สร้างต่างๆ ในแอปพลิเคชันสาธิตได้รับการใช้งานและจัดการกับสถานการณ์ที่ซับซ้อนมากขึ้นได้อย่างไร

บทช่วยสอน Swift MVVM: ดูตัวควบคุม

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

บางครั้งจำเป็นต้องอัปเดตองค์ประกอบ UI ที่มุมมองรากไม่ครอบคลุม เช่น ชื่อหรือปุ่มในแถบนำทาง นั่นเป็นเหตุผลที่ฉันมักจะยังคงลงทะเบียน view controller เป็นผู้ฟังเหตุการณ์เราเตอร์ถ้าฉันมีโมเดลการดูที่ครอบคลุมมุมมองทั้งหมดสำหรับตัวควบคุมการดูที่กำหนด ฉันส่งต่อโมเดลการดูไปยังมุมมองในภายหลัง แต่ก็ยังดีที่จะลงทะเบียน UIView ใดๆ เป็นผู้ฟังโดยตรง หากมีส่วนหนึ่งของหน้าจอที่มีอัตราการอัพเดทที่แตกต่างกัน เช่น ตัวแสดงหุ้นสดที่ด้านบนของเพจเกี่ยวกับบริษัทหนึ่งๆ

ตอนนี้โค้ดสำหรับ ChatsViewController สั้นมากจนใช้ไม่ถึงหนึ่งหน้า สิ่งที่เหลืออยู่คือการแทนที่มุมมองฐาน การเพิ่มและลบปุ่มเพิ่มจากแถบนำทาง การตั้งชื่อ การเพิ่มตัวเองเป็นผู้ฟัง และใช้โปรโตคอล ChatListListening :

 class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = "Chats" } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

ไม่มีอะไรเหลือที่สามารถทำได้ในที่อื่น เนื่องจาก ChatsViewController ถูกแยกให้เหลือน้อยที่สุด

บทช่วยสอน Swift MVVM: ดู

มุมมองในสถาปัตยกรรม MVVM ที่ไม่เปลี่ยนรูปยังคงค่อนข้างหนัก เนื่องจากยังมีรายการงานอยู่ แต่ฉันสามารถแยกความรับผิดชอบต่อไปนี้ออกได้เมื่อเปรียบเทียบกับสถาปัตยกรรม MVC:

  • การกำหนดสิ่งที่ต้องการเปลี่ยนแปลงเพื่อตอบสนองต่อสถานะใหม่
  • การนำผู้รับมอบสิทธิ์และฟังก์ชันสำหรับการดำเนินการไปใช้
  • จัดการทริกเกอร์การดูเพื่อดูเช่นท่าทางสัมผัสและแอนิเมชั่นที่เรียก
  • การแปลงข้อมูลในลักษณะที่สามารถแสดงได้ (เช่น Date s เป็น String s)

โดยเฉพาะข้อสุดท้ายได้เปรียบค่อนข้างมาก ใน MVC เมื่อ view หรือ view controller มีหน้าที่ในการแปลงข้อมูลสำหรับแสดงผล มันจะทำสิ่งนี้ใน main thread เสมอ เนื่องจากเป็นการยากมากที่จะแยกการเปลี่ยนแปลงจริงของ UI ที่จำเป็นต้องเกิดขึ้นบน thread นี้ออกจากสิ่งที่ ไม่จำเป็นต้องวิ่งบนนั้น และการเรียกใช้โค้ดที่ไม่เปลี่ยน UI บนเธรดหลักอาจทำให้แอปพลิเคชันตอบสนองน้อยลง

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

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

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

ตัวอย่างที่รู้จักกันดีคือการใช้งานแบบพิมพ์เพื่อค้นหา ซึ่งช่องค้นหาได้รับการอัปเดตด้วยผลลัพธ์ที่แม่นยำยิ่งขึ้นเมื่อเราป้อนข้อความมากขึ้น นี่คือวิธีที่ฉันจะใช้การเติมข้อความอัตโนมัติในคลาส CreateAutocompleteView : หน้าจอทั้งหมดให้บริการโดย CreateViewModel แต่กล่องข้อความกำลังฟัง AutocompleteContactViewModel แทน

อีกตัวอย่างหนึ่งคือการใช้ตัวตรวจสอบฟอร์ม ซึ่งสามารถสร้างเป็น "ลูปในเครื่อง" (การแนบหรือลบสถานะข้อผิดพลาดกับฟิลด์และประกาศฟอร์มให้ถูกต้อง) หรือทำผ่านการทริกเกอร์เหตุการณ์

โมเดลมุมมองที่ไม่เปลี่ยนแบบคงที่ให้การแยกที่ดีกว่า

ด้วยการใช้ MVVM แบบคงที่ เราได้จัดการแยกเลเยอร์ทั้งหมดออกจากกันในที่สุด เพราะตอนนี้โมเดลการดูเป็นสะพานเชื่อมระหว่างโมเดลและมุมมอง เรายังทำให้ง่ายต่อการจัดการเหตุการณ์ที่ไม่ได้เกิดจากการกระทำของผู้ใช้ และลบการพึ่งพาอาศัยกันจำนวนมากระหว่างส่วนต่างๆ ของแอปพลิเคชันของเรา สิ่งเดียวที่ตัวควบคุมการดูทำคือการลงทะเบียน (และยกเลิกการลงทะเบียน) กับตัวจัดการเหตุการณ์ในฐานะผู้ฟังสำหรับเหตุการณ์ที่ต้องการรับ

ประโยชน์:

  • ดูและดูการใช้งานคอนโทรลเลอร์มักจะเบากว่ามาก
  • ชั้นเรียนมีความเชี่ยวชาญและแยกจากกันมากขึ้น
  • เหตุการณ์สามารถเรียกได้ง่ายจากที่ใดก็ได้
  • เหตุการณ์เป็นไปตามเส้นทางที่คาดเดาได้ผ่านระบบ
  • สถานะอัพเดทจากที่เดียวเท่านั้น
  • App can be more performant as it's easier to do work off the main thread
  • Views receive tailor-made view models and are perfectly separated from the models

Downsides:

  • A full view model is created and sent every time the UI needs to update, often overwriting the same button text with the same button text, and replacing blocks with blocks that do exactly the same
  • Requires some helper extensions to make button taps and other UI events work well with the blocks in the view model
  • Event enum s can easily grow pretty large in complex scenarios and might be hard to split up

The great thing is that this is a pure Swift pattern: It does not require a third-party Swift MVVM framework, nor does it exclude the use of classic MVC, so you can easily add new features or refactor problematic parts of your application today without being forced to rewrite your whole application.

There are other approaches to combat large view controllers that provide better separation as well. I couldn't include them all in full detail to compare them, but let's take a brief look at some of the alternatives:

  • Some form of the MVVM pattern
  • Some form of Reactive (using RxSwift, sometimes combined with MVVM)
  • The model-view-presenter pattern (MVP)
  • The view-interactor-presenter-entity-router pattern (VIPER)

Traditional MVVM replaces most of the view controller code with a view model that is just a regular class and can be tested more easily in isolation. Since it needs to be a bi-directional bridge between the view and the model it often implements some form of Observables. That's why you often see it used together with a framework like RxSwift.

MVP and VIPER deal with extra abstraction layers between the model and the view in a more traditional way, while Reactive really remodels the way data and events flow through your application.

The Reactive style of programming is gaining a lot of popularity lately and actually is pretty close to the static MVVM approach with events, as explained in this article. The major difference is that it usually requires a framework, and a lot of your code is specifically geared towards that framework.

MVP is a pattern where both the view controller and the view are considered to be the view layer. The presenter transforms the model and passes it to the view layer, while I transform the data into a view model first. Since the view can be abstracted to a protocol, it's much easier to test.

VIPER takes the presenter from MVP, adds a separate “interactor” for business logic, calls the model layer “entity,” and has a router for navigation purposes (and to complete the acronym). It can be considered a more detailed and decoupled form of MVP.


So there you have it: static event-driven MVVM explained. I look forward to hearing from you in the comments below!

Related: Swift Tutorial: An Introduction to the MVVM Design Pattern