Statik Modellerle Çalışmak: Hızlı Bir MVVM Eğitimi

Yayınlanan: 2022-03-11

Bugün, gerçek zamanlı veriye dayalı uygulamalar için kullanıcılarımızdan gelen yeni teknik olanaklar ve beklentilerin, programlarımızı, özellikle de mobil uygulamalarımızı yapılandırma biçimimizde nasıl yeni zorluklar yarattığını göreceğiz. Bu makale iOS ve Swift hakkında olsa da, kalıpların ve sonuçların çoğu Android ve web uygulamaları için eşit derecede geçerlidir.

Modern mobil uygulamaların çalışma biçiminde son birkaç yılda önemli bir gelişme yaşandı. Daha yaygın internet erişimi ve anında iletme bildirimleri ve WebSockets gibi teknolojiler sayesinde, günümüzün birçok mobil uygulamasında kullanıcı genellikle çalışma zamanı olaylarının tek kaynağı değildir ve artık en önemli olanı değildir.

İki Swift tasarım modelinin her birinin modern bir sohbet uygulamasıyla ne kadar iyi çalıştığına daha yakından bakalım: klasik model-görünüm-denetleyici (MVC) kalıbı ve basitleştirilmiş bir değişmez model-görünüm-görüntü modeli (MVVM, bazen stilize edilmiş "ViewModel kalıbı". ”). Sohbet uygulamaları iyi bir örnektir çünkü birçok veri kaynağına sahiptirler ve veri alındığında kullanıcı arayüzlerini birçok farklı şekilde güncellemeleri gerekir.

Sohbet Uygulamamız

Bu Swift MVVM eğitiminde kılavuz olarak kullanacağımız uygulama, WhatsApp gibi sohbet uygulamalarından bildiğimiz temel özelliklerin çoğuna sahip olacak. Şimdi uygulayacağımız özellikleri gözden geçirelim ve MVVM ile MVC'yi karşılaştıralım. Uygulama:

  • Daha önce alınan sohbetleri diskten yükler
  • Bir GET isteği üzerinden mevcut sohbetleri sunucuyla senkronize eder
  • Kullanıcıya yeni bir mesaj gönderildiğinde push bildirimleri alacak
  • Sohbet ekranına girdiğimizde bir WebSocket'e bağlanacağız
  • Bir POST yeni bir mesaj gönderebilir
  • Şu anda içinde olmadığımız bir sohbetten yeni bir mesaj alındığında uygulama içi bir bildirim gösterecek
  • Mevcut sohbet için yeni bir mesaj aldığımızda hemen yeni bir mesaj gösterecek
  • Okunmamış bir mesajı okuduğumuzda okundu mesajı gönderir
  • Biri mesajımızı okuduğunda okundu mesajı alacak
  • Uygulama simgesindeki okunmamış mesajlar sayacı rozetini günceller
  • Alınan veya Çekirdek Veri olarak değiştirilen tüm mesajları senkronize eder

Bu demo uygulamada, Model uygulamasını biraz daha basit tutmak için gerçek bir API, WebSocket veya Core Data uygulaması olmayacak. Bunun yerine, konuşmaya başladığınızda size yanıt vermeye başlayacak bir sohbet robotu ekledim. Ancak, diğer tüm yönlendirmeler ve çağrılar, geri dönmeden önce küçük asenkron duraklamalar dahil olmak üzere, depolama ve bağlantılar gerçek olsaydı olacakları gibi uygulanır.

Aşağıdaki üç ekran oluşturulmuştur:

Sohbet Listesi, Sohbet Oluştur ve Mesajlar ekranları.

Klasik MVC

Her şeyden önce, bir iOS uygulaması oluşturmak için standart MVC modeli vardır. Apple, tüm belge kodunu ve API'lerin ve UI öğelerinin çalışmayı beklediği şekilde bu şekilde yapılandırır. Çoğu insanın bir iOS kursu aldığında öğrettiği şey budur.

Genellikle MVC, birkaç bin kod satırından oluşan şişirilmiş UIViewController s'ye yol açtığı için suçlanır. Ancak, her katman arasında iyi bir ayrımla iyi uygulanırsa, View s, Model s ve diğer Controller s arasında yalnızca ara yöneticiler gibi davranan oldukça ince ViewController s'ye sahip olabiliriz.

İşte uygulamanın MVC uygulaması için akış şeması (açıklık için CreateViewController dışarıda bırakarak):

MVC uygulama akış şeması, netlik için CreateViewController'ı dışarıda bırakır.

Katmanları ayrıntılı olarak inceleyelim.

modeli

Model katmanı genellikle MVC'deki en az sorunlu katmandır. Bu durumda, Chat ve Message nesneleri, harici veri kaynakları ve uygulamanın geri kalanı arasında aracılık yapmak için ChatWebSocket , ChatModel ve PushNotificationController kullanmayı seçtim. ChatModel , uygulama içindeki gerçeğin kaynağıdır ve bu demo uygulamasında yalnızca bellek içi çalışır. Gerçek hayattaki bir uygulamada, muhtemelen Core Data tarafından desteklenecektir. Son olarak, ChatEndpoint tüm HTTP çağrılarını yönetir.

Görünüm

Tüm görünüm kodlarını UIViewController s'den dikkatlice ayırdığım için birçok sorumluluğu üstlenmesi gerektiğinden görünümler oldukça büyüktür. Aşağıdakileri yaptım:

  • Görünümün şu anda hangi durumda olduğunu tanımlamak için (çok tavsiye edilen) durum enum modelini kullandı.
  • Düğmelere ve diğer eylemi tetikleyen arabirim öğelerine bağlanan işlevler eklendi (bir kişi adı girerken Geri Dön'e dokunmak gibi).
  • Kısıtlamaları ayarlayın ve her seferinde temsilciyi geri arayın.

Karışıma bir UITableView , görünümler artık UIViewController s'den çok daha büyüktür ve ChatView endişe verici 300'den fazla kod satırına ve birçok karışık göreve yol açar.

kontrolör

Model işleme mantığının ChatModel taşındığı için. Burada daha az optimal, ayrı projelerde gizlenebilecek olan tüm görünüm kodları artık görünümde yaşıyor, bu nedenle UIViewController s oldukça ince. Görünüm denetleyicisi, model verilerinin nasıl göründüğünden, nasıl getirildiğinden veya nasıl görüntülenmesi gerektiğinden tamamen habersizdir - yalnızca koordine eder. Örnek projede, UIViewController s'nin hiçbiri 150 satır koddan fazla gitmez.

Ancak, ViewController yine de aşağıdakileri yapar:

  • Görünüm ve diğer görünüm denetleyicileri için temsilci olmak
  • Gerekirse görünüm denetleyicilerini başlatma ve itme (veya patlatma)
  • ChatModel çağrı gönderme ve alma
  • Görünüm denetleyici döngüsünün aşamasına bağlı olarak WebSocket'i başlatma ve durdurma
  • Boşsa mesaj göndermemek gibi mantıklı kararlar vermek
  • Görünümün güncellenmesi

Bu hala çok fazla, ancak çoğunlukla koordinasyon, geri arama bloklarını işleme ve iletme.

Faydalar

  • Bu model herkes tarafından anlaşılır ve Apple tarafından tanıtılır
  • Tüm belgelerle çalışır
  • Ekstra çerçeveye gerek yok

Dezavantajlar

  • Görünüm denetleyicilerinin birçok görevi vardır; birçoğu temelde görünüm ve model katmanı arasında verileri ileri geri aktarıyor
  • Birden çok olay kaynağını işlemek için pek uygun değil
  • Sınıflar diğer sınıflar hakkında çok şey bilme eğilimindedir

Problem tanımı

Bu, Adobe Photoshop veya Microsoft Word gibi bir uygulamanın çalışacağını hayal ettiğiniz gibi, uygulama kullanıcının eylemlerini takip ettiği ve bunlara yanıt verdiği sürece çok iyi çalışır. Kullanıcı bir işlem yapar, kullanıcı arayüzü güncellenir, tekrarlanır.

Ancak modern uygulamalar, genellikle birden fazla yolla bağlantılıdır. Örneğin, bir REST API aracılığıyla etkileşime girersiniz, anında iletme bildirimleri alırsınız ve bazı durumlarda bir WebSocket'e de bağlanırsınız.

Bununla birlikte, aniden görünüm denetleyicisinin daha fazla bilgi kaynağıyla ilgilenmesi gerekir ve kullanıcı tarafından tetiklenmeden harici bir mesaj alındığında (WebSocket aracılığıyla bir mesaj almak gibi) bilgi kaynaklarının sağa dönüş yolunu bulması gerekir. denetleyicileri görüntüleyin. Bu, temelde aynı görevi gerçekleştirmek için her parçayı birbirine yapıştırmak için çok fazla kod gerektirir.

Dış Veri Kaynakları

Bir push mesajı aldığımızda neler olduğuna bir göz atalım:

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

Bir push bildirimi aldıktan sonra kendisini güncellemesi gereken bir görünüm denetleyicisi olup olmadığını anlamak için görünüm denetleyicileri yığınını manuel olarak kazmamız gerekiyor. Bu durumda, bu durumda yalnızca ChatsViewController olan UpdatedChatDelegate uygulayan ekranları da güncellemek istiyoruz. Bunu, bildirimi gizlememiz gerekip gerekmediğini bilmek için de yapıyoruz çünkü zaten bildirimin amaçlandığı Chat bakıyoruz. Bu durumda, nihayet mesajı görünüm denetleyicisine teslim ederiz. PushNotificationController yapabilmek için uygulama hakkında çok fazla şey bilmesi gerektiği oldukça açık.

ChatWebSocket , ChatViewController ile birebir ilişki kurmak yerine, uygulamanın diğer bölümlerine de mesajlar gönderiyor olsaydı, orada da aynı problemle karşı karşıya ChatViewController .

Başka bir harici kaynak eklediğimizde oldukça istilacı kod yazmamız gerektiği açık. Bu kod aynı zamanda oldukça kırılgandır, çünkü büyük ölçüde uygulama yapısına ve delegelerin verileri çalışmak için hiyerarşiye geri iletmesine dayanır.

Delegeler

MVC modeli, başka görünüm denetleyicileri eklediğimizde, karışıma ekstra karmaşıklık da ekler. Bunun nedeni, görünüm denetleyicilerinin, verileri ve referansları prepareForSegue delegeler, başlatıcılar ve - storyboard'lar söz konusu olduğunda - hazırlıkForSegue aracılığıyla birbirleri hakkında bilgi sahibi olma eğiliminde olmasıdır. Her görünüm denetleyicisi, modele veya aracı denetleyicilere olan kendi bağlantılarını yönetir ve her ikisi de güncellemeleri gönderir ve alır.

Ayrıca, görünümler, temsilciler aracılığıyla görünüm denetleyicilerine geri iletişim kurar. Bu işe yarasa da, verileri iletmek için atmamız gereken çok fazla adım olduğu anlamına geliyor ve kendimi her zaman geri aramalar etrafında çok fazla yeniden düzenleme yaparken ve delegelerin gerçekten ayarlanıp ayarlanmadığını kontrol ederken buluyorum.

ChatsListViewController'daki eski veriler gibi, ChatsListViewController artık updated(chat: Chat) ChatViewController için, kodu diğerinde değiştirerek bir görünüm denetleyicisini kırmak mümkündür. Özellikle daha karmaşık senaryolarda, her şeyi senkronize tutmak çok zor.

Görünüm ve Model Arasındaki Ayrım

Görünümle ilgili tüm kodu görünüm denetleyicisinden customView s'ye kaldırarak ve modelle ilgili tüm kodu özel denetleyicilere taşıyarak, görünüm denetleyicisi oldukça yalın ve ayrılmıştır. Ancak, hala bir sorun var: Görünümün göstermek istediği ile modelde bulunan veriler arasında bir boşluk var. İyi bir örnek ChatListView . Görüntülemek istediğimiz, bize kiminle konuştuğumuzu, son mesajın ne olduğunu, son mesaj tarihini ve Chat kaç tane okunmamış mesaj kaldığını söyleyen bir hücre listesi:

Sohbet ekranında okunmamış mesaj sayacı.

Ancak ne görmek istediğimizi bilmeyen bir modelden geçiyoruz. Bunun yerine, yalnızca bir kişiyle yapılan ve mesajları içeren bir Chat :

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

Artık, bize son mesajı ve mesaj sayısını alacak bazı ekstra kodları hızlı bir şekilde eklemek mümkündür, ancak tarihleri ​​dizgelere biçimlendirmek, kesinlikle görünüm katmanına ait bir görevdir:

 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 }

Sonunda, görüntülediğimizde ChatItemTableViewCell tarihi biçimlendiriyoruz:

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

Oldukça basit bir örnekte bile, görünümün ihtiyaç duyduğu ile modelin sağladığı arasında bir gerilim olduğu oldukça açık.

Statik Olaya Dayalı MVVM, diğer bir adıyla Statik Olaya Dayalı “ViewModel Modeli” Üzerine Alım

Statik MVVM, görünüm modelleriyle çalışır, ancak bunlar üzerinden çift yönlü trafik oluşturmak yerine - eskiden MVC'li görünüm denetleyicimizde yaptığımız gibi - bir olaya yanıt olarak kullanıcı arabiriminin her değişmesi gerektiğinde kullanıcı arabirimini güncelleyen değişmez görünüm modelleri oluştururuz .

Bir olay, olay enum için gerekli olan ilişkili verileri sağlayabildiği sürece, kodun hemen hemen her bölümü tarafından tetiklenebilir. Örneğin, received(new: Message) olayının alınması, bir anında iletme bildirimi, WebSocket veya normal bir ağ araması tarafından tetiklenebilir.

Bir şemada görelim:

MVVM uygulama akış şeması.

İlk bakışta, tam olarak aynı şeyi başarmak için çok daha fazla sınıf olduğundan, klasik MVC örneğinden biraz daha karmaşık görünüyor. Ancak daha yakından bakıldığında, ilişkilerin hiçbiri artık iki yönlü değildir.

Daha da önemlisi, kullanıcı arayüzündeki her güncelleme bir olay tarafından tetiklenir, bu nedenle olan her şey için uygulamada yalnızca bir yol vardır. Hangi olayları bekleyebileceğiniz hemen belli oluyor. Ayrıca, gerekirse yeni bir tane eklemeniz veya mevcut olaylara yanıt verirken yeni davranış eklemeniz gerektiği de açıktır.

Yeniden düzenlemeden sonra, yukarıda gösterdiğim gibi birçok yeni sınıfla sonuçlandım. Statik MVVM sürümü uygulamamı GitHub'da bulabilirsiniz. Ancak, değişiklikleri cloc aracıyla karşılaştırdığımda, aslında o kadar fazla ekstra kodun olmadığı ortaya çıkıyor:

Desen Dosyalar Boşluk Yorum kod
MVC 30 386 217 1807
OGVM 51 442 359 1981

Kod satırlarında yalnızca yüzde 9'luk bir artış var. Daha da önemlisi, bu dosyaların ortalama boyutu 60 satır koddan sadece 39'a düştü.

Kod satırları pasta grafikleri. Denetleyicileri görüntüleyin: MVC 287 ve MVVM 154 veya %47 daha az; Görüntülemeler: MVC 523 ve MVVM 392 veya %26 daha az.

Ayrıca en önemlisi, en büyük düşüşler genellikle MVC'de en büyük olan dosyalarda bulunabilir: görünümler ve görünüm denetleyicileri. Görünümler, orijinal boyutlarının yalnızca yüzde 74'ü ve görünüm denetleyicileri artık orijinal boyutlarının yalnızca yüzde 53'ü.

Ayrıca, ekstra kodun çoğunun, MVC'nin klasik @IBAction veya delege kalıplarına ihtiyaç duymadan görsel ağaçtaki düğmelere ve diğer nesnelere bloklar eklemeye yardımcı olan kitaplık kodu olduğuna da dikkat edilmelidir.

Gelin bu tasarımın farklı katmanlarını tek tek inceleyelim.

Etkinlik

Olay, genellikle ilişkili değerlerle birlikte her zaman bir enum . Genellikle, modelinizdeki varlıklardan biriyle örtüşürler, ancak böyle olması gerekmez. Bu durumda, uygulama iki ana olay enum bölünür: ChatEvent ve MessageEvent . ChatEvent , sohbet nesnelerinin kendisindeki tüm güncellemeler içindir:

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

Diğeri, Mesajla ilgili tüm olaylarla ilgilenir:

 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 makul bir boyutla sınırlamanız önemlidir. 10 veya daha fazla vakaya ihtiyacınız varsa, bu genellikle birden fazla konuyu kapsamaya çalıştığınızın bir işaretidir.

Not: enum kavramı Swift'de son derece güçlüdür. enum s'yi ilişkili değerlerle çok kullanma eğilimindeyim, çünkü aksi takdirde isteğe bağlı değerlerle sahip olacağınız çok fazla belirsizliği ortadan kaldırabilirler.

Swift MVVM Eğitimi: Olay Yönlendiricisi

Olay yönlendiricisi, uygulamada gerçekleşen her olay için giriş noktasıdır. İlişkili değeri sağlayabilen herhangi bir sınıf, bir olay oluşturabilir ve bunu olay yönlendiricisine gönderebilir. Böylece herhangi bir kaynak tarafından tetiklenebilirler, örneğin:

  • Kullanıcı, belirli bir görünüm denetleyicisine geçiş yapıyor
  • Belirli bir düğmeye dokunarak kullanıcı
  • uygulama başlıyor
  • Aşağıdakiler gibi harici olaylar:
    • Bir hata veya yeni verilerle geri dönen bir ağ isteği
    • Push bildirimleri
    • WebSocket mesajları

Olay yönlendiricisi, olayın kaynağı hakkında mümkün olduğunca az şey bilmeli ve tercihen hiçbir şey bilmemelidir. Bu örnek uygulamadaki olayların hiçbirinin nereden geldiklerine dair herhangi bir gösterge yoktur, bu nedenle herhangi bir tür mesaj kaynağına karıştırılması çok kolaydır. Örneğin, WebSocket yeni bir anında iletme bildirimi olarak aynı olayı received(message: Message, contact: String) tetikler.

Olaylar (zaten tahmin ettiniz), bu olayları daha fazla işlemesi gereken sınıflara yönlendirilir. Genellikle çağrılan sınıflar yalnızca model katmanı (verilerin eklenmesi, değiştirilmesi veya kaldırılması gerekiyorsa) ve olay işleyicisidir. Her ikisini de biraz daha ileride tartışacağım, ancak olay yönlendiricisinin ana özelliği, tüm olaylara tek bir kolay erişim noktası vermesi ve işi diğer sınıflara iletmesidir. Örnek olarak 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) } } }

Burada oldukça az şey oluyor: Yaptığımız tek şey modeli güncellemek ve olayı ChatEventHandler , böylece kullanıcı arayüzü güncellenir.

Swift MVVM Eğitimi: Model Kontrolörü

Bu, zaten oldukça iyi çalıştığı için MVC'de kullandığımızla tamamen aynı sınıftır. Uygulamanın durumunu temsil eder ve genellikle Core Data veya yerel bir depolama kitaplığı tarafından desteklenir.

Model katmanları - MVC'de doğru bir şekilde uygulanırsa - farklı kalıplara uyması için çok nadiren yeniden düzenlemeye ihtiyaç duyar. En büyük değişiklik, modeli değiştirmenin daha az sınıftan gerçekleşmesidir, bu da değişikliklerin nerede gerçekleştiğini biraz daha net hale getirir.

Bu modele alternatif olarak, modeldeki değişiklikleri gözlemleyebilir ve bunların ele alındığından emin olabilirsiniz. Bu durumda, yalnızca *EventRouter ve *Endpoint sınıflarının modeli değiştirmesine izin vermeyi seçtim, bu nedenle modelin nerede ve ne zaman güncelleneceği konusunda net bir sorumluluk var. Buna karşılık, değişiklikleri gözlemliyor olsaydık, hatalar gibi modeli değiştirmeyen olayları ChatEventHandler aracılığıyla yaymak için ek kodlar yazmamız gerekirdi, bu da olayların uygulamada nasıl aktığını daha az belirgin hale getirirdi.

Swift MVVM Eğitimi: Olay İşleyici

Olay işleyicisi, ChatEventRouter üzerinde bir işlev çağırdığında oluşturulan güncellenmiş görünüm modellerini almak için görünümlerin veya görünüm denetleyicilerinin kendilerini dinleyici olarak kaydedebilecekleri (ve kaydını silebilecekleri) ChatEventHandler .

Daha önce MVC'de kullandığımız tüm görünüm durumlarını kabaca yansıttığını görebilirsiniz. Ses veya Taptic motorunu tetikleme gibi başka türde UI güncellemeleri istiyorsanız, buradan da yapılabilir.

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

Bu sınıf, belirli bir olay gerçekleştiğinde doğru dinleyicinin doğru görünüm modelini almasını sağlamaktan başka bir şey yapmaz. Yeni dinleyiciler, ilk durumlarını ayarlamak için gerekirse, eklendiklerinde hemen bir görünüm modeli alabilirler. Tutma döngülerini önlemek için her zaman listeye weak bir referans eklediğinizden emin olun.

Swift MVVM Eğitimi: Modeli Görüntüle

Bir çok MVVM modelinin yaptığı ile statik değişkenin yaptığı arasındaki en büyük farklardan biri burada. Bu durumda, görünüm modeli, kendisini model ve görünüm arasında kalıcı bir iki yönlü bağlı ara olarak kurmak yerine değişmezdir. Bunu neden yapalım ki? Bunu açıklamak için biraz duralım.

Olası tüm durumlarda iyi çalışan bir uygulama oluşturmanın en önemli yönlerinden biri, uygulamanın durumunun doğru olduğundan emin olmaktır. Kullanıcı arayüzü modelle eşleşmiyorsa veya güncel olmayan veriler varsa, yaptığımız her şey hatalı verilerin kaydedilmesine veya uygulamanın çökmesine veya beklenmedik bir şekilde davranmasına neden olabilir.

Bu kalıbı uygulamanın amaçlarından biri, kesinlikle gerekli olmadıkça uygulamada hiçbir durumumuz olmamasıdır. Devlet tam olarak nedir? Devlet, temel olarak, belirli bir veri türünün temsilini depoladığımız her yerdir. Özel bir durum türü, kullanıcı arabiriminizin şu anda içinde bulunduğu durumdur; bu, elbette kullanıcı arabirimi odaklı bir uygulamayla önleyemeyeceğimiz durumdur. Diğer durum türlerinin tümü veriyle ilgilidir. Sohbet Listesi ekranında UITableView yedekleyen bir Chat dizisinin bir kopyasına sahipsek, bu yinelenen duruma bir örnektir. Geleneksel iki yönlü bir görünüm modeli, kullanıcılarımızın Chat 'lerinin bir kopyasına başka bir örnek olabilir.

Her model değişikliğinde yenilenen değişmez bir görünüm modelini geçerek, bu tür yinelenen durumu ortadan kaldırıyoruz, çünkü kendisini UI'ye uyguladıktan sonra artık kullanılmaz. O zaman önleyemeyeceğimiz yalnızca iki tür duruma sahibiz - kullanıcı arayüzü ve model - ve bunlar birbirleriyle mükemmel bir uyum içindedir.

Dolayısıyla buradaki görünüm modeli, bazı MVVM uygulamalarından oldukça farklıdır. Yalnızca görünümün modelin durumunu yansıtması için gereken tüm bayraklar, değerler, bloklar ve diğer değerler için değişmez bir veri deposu görevi görür, ancak Görünüm tarafından hiçbir şekilde güncellenemez.

Bu nedenle basit bir değişmez struct olabilir. Bu struct olabildiğince basit tutmak için, onu bir görünüm modeli oluşturucu ile somutlaştıracağız. Bir görünüm modeliyle ilgili ilginç şeylerden biri, daha önce görünümde bulunan durum enum mekanizmasının yerine shouldShowBusy ve shouldShowError gibi davranışsal bayraklar almasıdır. İşte daha önce analiz ettiğimiz ChatItemTableViewCell verileri:

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

Görünüm modeli oluşturucu, görünümün ihtiyaç duyduğu tam değerler ve eylemlerle zaten ilgilendiğinden, tüm veriler önceden biçimlendirilir. Ayrıca yeni, bir öğeye dokunulduğunda tetiklenecek bir bloktur. Görünüm modeli oluşturucu tarafından nasıl yapıldığını görelim.

Model Oluşturucuyu Görüntüle

Görünüm modeli oluşturucu, Chat s veya Message s gibi girdileri belirli bir görünüm için mükemmel şekilde uyarlanmış görünüm modellerine dönüştürerek görünüm modelleri örnekleri oluşturabilir. Görünüm modeli oluşturucusunda gerçekleşen en önemli şeylerden biri, görünüm modelindeki blokların içinde gerçekte ne olduğunu belirlemektir. Görünüm modeli oluşturucu tarafından eklenen bloklar, mimarinin diğer bölümlerinin işlevlerini mümkün olan en kısa sürede çağırarak son derece kısa olmalıdır. Bu tür blokların herhangi bir iş mantığı olmamalıdır.

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

Artık tüm ön biçimlendirme aynı yerde gerçekleşir ve davranışa burada da karar verilir. Bu hiyerarşide oldukça önemli bir sınıftır ve demo uygulamasındaki farklı oluşturucuların nasıl uygulandığını ve daha karmaşık senaryolarla nasıl başa çıktığını görmek ilginç olabilir.

Swift MVVM Eğitimi: Denetleyiciyi Görüntüle

Bu mimarideki görünüm denetleyicisi çok az şey yapar. Görüşü ile ilgili her şeyi kuracak ve yıkacak. Dinleyicileri doğru zamanda eklemek ve kaldırmak için gereken tüm yaşam döngüsü geri aramalarını aldığı için bunu yapmak en uygunudur.

Bazen, gezinme çubuğundaki başlık veya düğme gibi kök görünümün kapsamadığı bir UI öğesini güncellemesi gerekir. Bu nedenle, verilen görünüm denetleyicisi için tüm görünümü kapsayan bir görünüm modelim varsa, genellikle görünüm denetleyicisini olay yönlendiricisine bir dinleyici olarak kaydederim; Görünüm modelini daha sonra görünüme iletirim. Ancak, ekranın farklı bir güncelleme hızına sahip bir bölümü varsa, örneğin belirli bir şirketle ilgili bir sayfanın üstünde canlı bir hisse senedi kaydı varsa, herhangi bir UIView doğrudan dinleyici olarak kaydetmek de iyidir.

ChatsViewController kodu artık o kadar kısa ki bir sayfadan daha az zaman alıyor. Geriye, temel görünümü geçersiz kılmak, gezinme çubuğuna ekle düğmesini eklemek ve kaldırmak, başlığı ayarlamak, kendisini bir dinleyici olarak eklemek ve ChatListListening protokolünü uygulamak kalıyor:

 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 için başka yerde yapılabilecek hiçbir şey kalmadı.

Swift MVVM Eğitimi: Görüntüle

Değişmez MVVM mimarisindeki görünüm, hala bir görev listesine sahip olduğu için oldukça ağır olabilir, ancak MVC mimarisine kıyasla onu aşağıdaki sorumluluklardan çıkarmayı başardım:

  • Yeni bir duruma yanıt olarak nelerin değişmesi gerektiğini belirleme
  • Eylemler için temsilciler ve işlevler uygulama
  • Hareketler ve tetiklenen animasyonlar gibi görüntülemeden görüntülemeye tetikleyicileri kullanın
  • Verileri gösterilebilecek şekilde dönüştürme ( Date s'den String s'ye gibi)

Özellikle son noktanın oldukça büyük bir avantajı var. MVC'de, görüntüleme veya görüntüleme denetleyicisi verileri görüntüleme için dönüştürmekten sorumlu olduğunda, bunu her zaman ana iş parçacığında yapacaktır, çünkü bu iş parçacığında gerçekleşmesi gereken UI'de gerçek değişiklikleri ayırmak çok zordur. üzerinde çalıştırmak için gerekli değildir. Ana iş parçacığında çalışan kullanıcı arabirimi değişikliği olmayan kodun olması, daha az yanıt veren bir uygulamaya yol açabilir.

Bunun yerine, bu MVVM modeliyle, bir dokunuşla tetiklenen bloktan görünüm modelinin oluşturulduğu ve dinleyiciye aktarılacağı ana kadar her şey - bunların hepsini ayrı bir iş parçacığında çalıştırabilir ve yalnızca ana iş parçacığına daldırabiliriz. UI güncellemeleri yapmak için son. Uygulamamız ana iş parçacığında daha az zaman harcarsa daha sorunsuz çalışacaktır.

Görünüm modeli yeni durumu görünüme uyguladığında, başka bir durum katmanı olarak oyalanmak yerine buharlaşmasına izin verilir. Bir olayı tetikleyebilecek her şey, görünümdeki bir öğeye eklenir ve görünüm modeline geri iletişim kurmayacağız.

Bir şeyi hatırlamak önemlidir: Bir görünüm modelini bir görünüm denetleyicisi aracılığıyla bir görünüme eşlemek zorunda değilsiniz. Daha önce de belirtildiği gibi, özellikle güncelleme oranları değiştiğinde görünümün bölümleri diğer görünüm modelleri tarafından yönetilebilir. Bir sohbet bölmesini ortak çalışanlar için açık tutarken farklı kişiler tarafından düzenlenen bir Google E-Tablosunu düşünün; bir sohbet mesajı geldiğinde belgeyi yenilemek pek kullanışlı değildir.

İyi bilinen bir örnek, biz daha fazla metin girdikçe arama kutusunun daha doğru sonuçlarla güncellendiği bir tür bul uygulamasıdır. Otomatik tamamlamayı CreateAutocompleteView sınıfında şu şekilde uygulardım: Tüm ekran CreateViewModel tarafından sunulur, ancak metin kutusu bunun yerine AutocompleteContactViewModel dinler.

Başka bir örnek, bir "yerel döngü" olarak oluşturulabilen (alanlara hata durumları ekleme veya kaldırma ve bir formun geçerli olduğunu bildirme) veya bir olayı tetikleyerek yapılabilen bir form doğrulayıcı kullanmaktır.

Statik Değişmez Görünüm Modelleri Daha İyi Ayırma Sağlar

Statik bir MVVM uygulaması kullanarak nihayet tüm katmanları tamamen ayırmayı başardık çünkü görünüm modeli artık model ve görünüm arasında köprü oluşturuyor. Ayrıca, kullanıcı eyleminin neden olmadığı olayları yönetmeyi kolaylaştırdık ve uygulamamızın farklı bölümleri arasındaki birçok bağımlılığı kaldırdık. Bir görünüm denetleyicisinin yaptığı tek şey, kendisini almak istediği olaylar için bir dinleyici olarak olay işleyicilerine kaydetmek (ve kaydını silmek).

Faydalar:

  • Görüntüleme ve görüntüleme denetleyici uygulamaları çok daha hafif olma eğilimindedir
  • Sınıflar daha uzmanlaşmış ve ayrılmış
  • Olaylar herhangi bir yerden kolayca tetiklenebilir
  • Olaylar sistem içinde öngörülebilir bir yol izler
  • Durum yalnızca bir yerden güncellenir
  • 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