Kombinasikan MVVM berbasis di Aplikasi UIKit dan SwiftUI untuk Pengembang UIKit



Kita tahu bahwa ObservableObjeckelas t dengan @Publishedpropertinya dibuat Combinekhusus untuk View Modeldalam SwiftUI. Tetapi hal yang persis sama View Modeldapat digunakan dalam UIKitimplementasi arsitektur  MVVM, meskipun dalam hal ini kita harus secara manual "mengikat" ( bind) UIelemen - elemen ke @Published properti View Model. Anda akan terkejut, tetapi dengan bantuan Combineini Anda dapat melakukan beberapa baris kode. Selain itu, mengikuti ideologi ini saat mendesain UIKitaplikasi, Anda kemudian akan dengan mudah beralih ke SwiftUI.

Tujuan artikel ini adalah untuk menunjukkan dengan contoh sederhana sederhana bagaimana Anda dapat mengimplementasikan MVVMarsitektur UIKitdengan elegan  Combine. Sebagai kontras, kami menunjukkan penggunaan yang samaView Model c SwiftUI.

Artikel ini akan membahas dua aplikasi sederhana yang memungkinkan Anda memilih informasi cuaca terbaru untuk kota tertentu dari situs p OpenWeatherMa . Tetapi UIsalah satunya akan dibuat dengan aplikasi SwiftUI, dan yang lainnya dengan bantuan UIKit. Bagi pengguna, aplikasi ini akan terlihat hampir sama.



Kode ada di Github .

Antarmuka pengguna ( UI) hanya akan berisi 2 UI elemen: bidang teks untuk memasuki kota dan label untuk menampilkan suhu. Bidang teks untuk memasuki kota adalah INPUT aktif ( Input), dan label suhu adalah EXIT pasif ( Output).  

Peran View Model dalam arsitektur MVVMadalah ia mengambil INPUT dari View(atau ViewControllerke UIKit), mengimplementasikan logika bisnis aplikasi dan meneruskan OUTPUT ke  View(atau ViewControllerke UIKit), mungkin menyajikan data ini dalam format yang diinginkan.

Menciptakan  View Modeldengan Combineapa pun jenis logika bisnis - sinkron atau asinkron - sangat sederhana jika Anda menggunakan ObservableObject kelas dengan @Publishedpropertinya.

API OpenWeatherMap


Meskipun layanan  OpenWeatherMap   memungkinkan Anda memilih informasi cuaca yang sangat luas, Model data yang kami minati akan sangat sederhana, namun memberikan informasi terperinci  WeatherDetailtentang cuaca saat ini di kota yang dipilih dan terletak di file  Model.swift :



Meskipun dalam tugas khusus ini kami hanya akan tertarik pada suhu temp, yang ada dalam struktur  Main, Model menyediakan informasi lengkap lengkap tentang cuaca saat ini sebagai struktur root  WeatherDetail, percaya bahwa di masa depan Anda akan ingin memperluas kemampuan aplikasi ini. Strukturnya WeatherDetail adalah Codable, ini akan memungkinkan kita untuk mendekode JSONdata secara harfiah ke dalam Model hanya dengan dua baris kode  .

Strukturnya  WeatherDetail juga harusIdentifiablejika kita ingin membuat lebih mudah bagi diri kita sendiri di masa depan untuk menampilkan array prakiraan cuaca  [WeatherDetail] selama beberapa hari di muka dalam bentuk daftar  List dari SwiftUI. Ini juga kosong untuk aplikasi cuaca terkini yang lebih canggih di masa depan. Protokol Identifiablemensyaratkan keberadaan properti id,yang sudah kita miliki, sehingga tidak ada upaya tambahan yang diperlukan dari kami.

Biasanya, layanan, termasuk layanan  OpenWeatherMap , menawarkan semua jenis layanan URLs untuk mendapatkan sumber daya yang kita butuhkan. Layanan  OpenWeatherMap menawarkan kami URLsuntuk mengambil informasi terperinci tentang cuaca saat ini atau perkiraan selama 5 hari di kota tertentu city. Dalam aplikasi ini, kami hanya akan tertarik pada informasi cuaca terkini dan untuk kasus iniURLdihitung menggunakan fungsi absoluteURL (city: String):



API untuk layanan OpenWeatherMap ,  kami akan meletakkannya di file WeatherAPI.swift . Bagian pusatnya akan menjadi metode untuk memilih informasi cuaca rinci  WeatherDetaildi kota  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

Dalam konteks kerangka kerja, Combine metode ini menghasilkan tidak hanya informasi cuaca rinci  WeatherDetail, tetapi "penerbit" yang sesuai Publisher. "Penerbit" AnyPublisher<WeatherDetail, Never>kami tidak mengembalikan kesalahan apa pun - Neverdan jika kesalahan pengambilan sampel atau pengkodean masih terjadi, maka wakil kembali  WeatherDetail.placeholdertanpa pesan tambahan apa pun tentang penyebab kesalahan tersebut. 

Pertimbangkan lebih detail metode  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>yang memilih  informasi cuaca terperinci untuk kota dari situs web OpenWeatherMap city dan tidak mengembalikan kesalahan apa pun Never:



  1. berdasarkan nama kota, kami  city membentuk URL menggunakan fungsi absoluteURL(city:city)untuk meminta informasi cuaca rinci  WeatherDetail,
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

"Penerbit" yang tidak sinkron itu memperoleh AnyPublisher"tidak lepas landas" dengan sendirinya, ia tidak memberikan apa pun sampai seseorang "berlangganan" ke sana. Kami akan menggunakannya dalam  ObservableObject kelas yang berperan View Modeldalam  SwiftUIkeduanya dan UIKit

Buat Lihat Model


Untuk View Modelmembuat kelas TempViewModelyang sangat sederhana  yang mengimplementasikan protokol ObservableObject dengan dua  @Published properti:  



  1. satu  @Published var city: Stringadalah kota (Anda secara kondisional dapat menyebutnya sebagai MASUK, karena nilainya diatur oleh pengguna pada View),  
  2. yang kedua  @Published var currentWeather = WeatherDetail.placeholder adalah cuaca di kota ini saat ini (kami dapat memanggil kondisional ini sebagai EXIT, karena ini diperoleh dengan mengambil data dari situs web  OpenWeatherMap ).

Setelah kami menetapkan  @Published properti  city, kami dapat mulai menggunakannya baik sebagai properti sederhana  citymaupun sebagai "penerbit"  $city.

Di kelas  TempViewModel, Anda tidak hanya dapat mendeklarasikan properti yang menarik bagi kami, tetapi juga meresepkan logika bisnis dari interaksinya. Untuk tujuan ini, ketika menginisialisasi instance kelas  TempViewModel , init?kita dapat membuat "berlangganan" yang akan beroperasi di seluruh "siklus hidup" instance kelas  TempViewModeldan mereproduksi ketergantungan cuaca saat ini  currentWeather di kota  city.

Untuk melakukan ini, Combinekami merentangkan rantai dari "penerbit" input $city ke output "penerbit" AnyPublisher<WeatherDetail, Never>, yang nilainya adalah cuaca saat ini. Selanjutnya, kami "berlangganan" dengan bantuan "pelanggan" assign (to: \.currentWeather, on: self) dan dapatkan nilai cuaca saat ini yang diinginkan  currentWeather sebagai @Published properti "output"  .

Kita harus menarik rantai TIDAK hanya dari properti city, yaitu dari "penerbit" $cityyang akan berpartisipasi dalam pembuatan UI dan di sanalah kita akan mengubahnya.

Bagaimana kita akan melakukan ini?

Kami sudah memiliki fungsi di gudang senjata kami fetchWeather (for city: String)yang ada di kelas  WeatherAPIdan mengembalikan "penerbit" AnyPublisher<WeatherDetail, Never> dengan informasi cuaca terperinci tergantung pada kota  city, dan kami hanya bisa menggunakan nilai "penerbit" itu  $cityuntuk mengubahnya menjadi argumen fungsi ini.

 Buka penerbit yang tepat  fetchWeather (for city: String) untuk  Combinemembantu kami operator  flatMap:



OperatorflatMapmembuat "penerbit" baru berdasarkan data yang diterima dari "penerbit" sebelumnya.

Selanjutnya, kami "berlangganan" ke "penerbit" yang baru diterima ini dengan bantuan "pelanggan" yang sangat sederhana  assign (to: \.currentWeather, on: self)dan menetapkan nilai yang diterima dari "penerbit" untuk @Publishedproperti  currentWeather:



Kami baru saja membuat init( )"penerbit" ASYNCHRONOUS dan "berlangganan" untuknya, menghasilkan AnyCancellable"berlangganan" ".

AnyCancellable "Berlangganan" memungkinkan penelepon untuk membatalkan "berlangganan" kapan saja dan tidak lagi menerima nilai dari "penerbit", tetapi terlebih lagi, segera setelah  AnyCancellable"berlangganan" meninggalkan ruang lingkupnya, memori yang ditempati oleh "penerbit" dibebaskan. Karena itu, segera setelah init( ) selesai, "langganan" ini akan dihapus oleh sistem ARC, dan tidak memiliki waktu untuk menetapkan informasi asinkron tentang cuaca saat ini yang diterima dengan penundaan waktu  currentWeather. Untuk menyimpan "langganan" seperti itu, perlu untuk membuat init()variabel LUAR var cancellableSetyang akan membuat AnyCancellable"langganan" kami dalam variabel ini sepanjang seluruh "siklus hidup" instance kelas  TempViewMode

The AnyCancellable“langganan” dalam variabel disimpan cancellableSetmenggunakan operator  store ( in: &self.cancellableSet):



Sebagai hasilnya, “langganan” akan dipertahankan di seluruh “siklus hidup” dari contoh kelas  TempViewModel. Kami dapat mengubah nilai penerbit seperti yang diinginkan $city, dan cuaca saat currentWeather ini untuk kota ini akan selalu siap membantu kami .

Untuk mengurangi jumlah panggilan server saat mengetik kota city, kita tidak boleh menggunakan secara langsung "penerbit" dari baris dengan nama kota  $city, tetapi versi yang dimodifikasi dengan operator debouncedan removeDuplicates:



Operator  debounce digunakan untuk menunggu sampai pengguna selesai mengetik informasi yang diperlukan pada keyboard, dan hanya kemudian melakukan tugas sumber daya intensif sekali.

Demikian pula, operator removeDuplicatesakan mempublikasikan nilai hanya jika mereka berbeda dari nilai sebelumnya. Misalnya, jika pengguna pertama kali masuk john, lalu joe, dan sekali lagi john, kami johnhanya akan menerima sekali. Ini membantu membuat kita UIlebih efisien.

Membuat UI dengan SwiftUI


Sekarang kita sudah memilikinya View Model, mari kita mulai UI. Pertama di SwiftUI, dan kemudian di UIKit.

Di kami Xcodemembuat proyek baru dengan SwiftUIdan dalam struktur yang dihasilkan kami ContentView  menempatkan kami  View Modelsebagai @ObservedObject variabel model. Ganti  Text ("Hello, World!") dengan judul  Text ("WeatherApp"), tambahkan kotak teks untuk memasuki kota  TextField ("City", text: self.$model.city) dan label untuk menampilkan suhu:



Kami langsung menggunakan nilai-nilai variabel kami model: TempViewModel(). Kami digunakan di kotak teks untuk memasuki kota $model.city, dan pada label untuk menampilkan suhu - model.currentWeather.main?.temp.

Sekarang, setiap perubahan pada  @Published properti akan mengarah ke "menggambar ulang" View:



Ini dipastikan oleh kenyataan bahwa kita View Model adalah@ObservedObject, yaitu, "mengikat" OTOMATIS ( binding) @Publisheddari properti kami View Modeldan elemen antarmuka pengguna ( UI) dilakukan . "Ikatan" OTOMATIS semacam itu hanya dimungkinkan di SwiftUI.

Membuat UI dengan UIKit


Apa yang harus dilakukan dengan ini UIKit? Itu tidak ada di sana  @ObservedObject. Di  UIKit kami akan melakukan "mengikat" ( binding) secara manual. Ada banyak cara untuk melakukan "pengikatan manual" ini:

  • Key-Value Observing atau KVO: mekanisme penggunaan  key pathsuntuk memantau properti dan menerima pemberitahuan bahwa properti telah berubah.
  • Pemrograman reaktif fungsional atau FRP: penggunaan suatu kerangka kerja Combine.
  • Delegation: Menggunakan metode delegasi untuk mengirim pemberitahuan bahwa nilai properti telah berubah.
  • Boxing: didSet { } , .

Diberi judul artikel, kami secara alami akan bekerja di lapangan Combine. Dalam UIKitaplikasi, kami akan menunjukkan betapa mudahnya membuat "penjilidan manual" dengannya Combine.

Dalam UIKitaplikasi, kita juga akan memiliki dua UI elemen: UITextFielduntuk memasuki kota dan UILabeluntuk menampilkan suhu. Secara ViewControlleralami kita akan memiliki Outletelemen-elemen ini:





Dalam bentuk variabel biasa viewModel, kita memiliki yang sama seperti View Modelpada bagian sebelumnya:



Sebelum melakukan "pengikatan manual" dengan Combine, mari kita buat bidang teks UITextFieldsekutu kita dan "penerbit" konten kita text:



Ini akan memungkinkan kita untuk dengan mudah viewDidLoadmengimplementasikan "penjilidan manual" menggunakan fungsi inibinding ():



Memang, kami "berlangganan" ke "penerbit" cityTextField.textPublisherdengan bantuan "pelanggan" yang sangat sederhana  assign (to: \.city, on: viewModel)dan menetapkan teks yang diketik oleh pengguna di bidang teks cityTextFieldke @Publishedproperti "input"  milik citykami View Model.

Selain itu, kami melakukan perubahan ke arah lain: kita “berlangganan” ke “output”  @Publishedproperti  $currentWeather dengan bantuan dari “pelanggan” sink dan penutupannya receiveValue, membentuk nilai suhu dan menetapkan ke label temperatureLabel.

Diterima di  viewDidLoad "berlangganan" disimpan dalam variabel var cancellableSet. Setelah membuatnya sekali, kami mengizinkan mereka untuk bertindak di seluruh "siklus hidup" dari instance kelas ViewControllerdan bersama dengan "berlangganan" di View Modelmengimplementasikan semua logika bisnis aplikasi.

By the way, protokolnya ObservableObjecttidak berfungsi UIKit, tetapi tidak mengganggu. UIKit sama sekali tidak peduli dengan protokol ObservableObjectdan, pada prinsipnya, itu dapat dihapus  View Modeldalam UIKit aplikasi:



Tapi kami tidak akan melakukan ini, karena kami ingin tetap tidak berubah View Modeluntuk kedua aplikasi saat ini UIKitdan mungkin aplikasi masa depan aktif SwiftUI.

Itu saja. Kode ada di Github .

Kesimpulan

Kerangka kerja reaktif fungsional Combinememungkinkan Anda untuk mengimplementasikan MVVMarsitektur dengan sederhana dan ringkas baik SwiftUIdalam UIKitbentuk kode yang dapat dimengerti maupun dibaca.

Tautan:

Gabungkan + UIKit + MVVM
Menggunakan Combine
iOS MVVM Tutorial: Refactoring dari MVC
MVVM dengan Combine Tutorial untuk iOS

PS Jika Anda ingin melihat beberapa informasi cuaca, Anda harus mendaftar di OpenWeatherMap  dan mendapatkannya API key. Proses ini akan membawa Anda tidak lebih dari 2 menit.

All Articles