Arsitektur komponen UI dalam aplikasi iOS



Halo, Habr!

Nama saya Valera, dan selama dua tahun sekarang saya telah mengembangkan aplikasi iOS sebagai bagian dari tim Badoo. Salah satu prioritas kami adalah menjaga kode dengan mudah. Karena banyaknya fitur baru yang jatuh ke tangan kita setiap minggu, kita harus terlebih dahulu memikirkan arsitektur aplikasi, jika tidak, akan sangat sulit untuk menambahkan fitur baru ke produk tanpa merusak yang sudah ada. Jelas, ini juga berlaku untuk implementasi antarmuka pengguna (UI) terlepas dari apakah ini dilakukan dengan menggunakan kode, Xcode (XIB) atau pendekatan campuran. Pada artikel ini saya akan menjelaskan beberapa teknik implementasi UI yang memungkinkan kami untuk menyederhanakan pengembangan antarmuka pengguna, membuatnya fleksibel dan nyaman untuk pengujian. Ada juga versi bahasa Inggris dari artikel ini.

Sebelum Anda mulai ...


Saya akan mempertimbangkan teknik implementasi antarmuka pengguna menggunakan contoh aplikasi yang ditulis dalam Swift. Aplikasi di klik tombol menunjukkan daftar teman.

Ini terdiri dari tiga bagian:

  1. Komponen adalah komponen UI khusus, yaitu kode yang hanya terkait dengan antarmuka pengguna.
  2. Aplikasi demo - model tampilan demo dan entitas antarmuka pengguna lain yang hanya memiliki dependensi UI.
  3. Aplikasi sebenarnya adalah model tampilan dan entitas lain yang mungkin berisi dependensi dan logika tertentu.

Mengapa ada pemisahan seperti itu? Saya akan menjawab pertanyaan ini di bawah, tetapi untuk sekarang, lihat antarmuka pengguna aplikasi kami:


Ini adalah tampilan sembul dengan konten di atas tampilan layar penuh lainnya. Semuanya sederhana.

Kode sumber lengkap proyek ini tersedia di GitHub .

Sebelum mempelajari kode UI, saya ingin memperkenalkan Anda ke kelas bantu Observable yang digunakan di sini. Antarmukanya terlihat seperti ini:

var value: T func observe(_ closure: @escaping (_ old: T, _ new: T) -> Void) -> ObserverProtocol func observeNewAndCall(_ closure: @escaping (_ new: T) -> Void) -> ObserverProtocol 

Ini hanya memberitahukan semua pengamat perubahan yang ditandatangani sebelumnya, jadi ini adalah semacam alternatif untuk KVO (mengamati nilai-kunci) atau, jika Anda suka, pemrograman reaktif. Ini adalah contoh penggunaan:

 self.observers.append(self.viewModel.items.observe { [weak self] (_, newItems) in   self?.state = newItems.isEmpty ? .zeroCase(type: .empty) : .normal   self?.collectionView.reloadSections(IndexSet(integer: 0)) }) 

Pengontrol berlangganan perubahan ke properti self.viewModel.items , dan ketika perubahan terjadi, pawang mengeksekusi logika bisnis. Misalnya, memperbarui kondisi tampilan dan memuat kembali tampilan koleksi dengan item baru.

Anda akan melihat lebih banyak contoh penggunaan di bawah ini.

Metodologi


Di bagian ini saya akan berbicara tentang empat teknik pengembangan UI yang digunakan di Badoo:

1. Implementasi antarmuka pengguna dalam kode.

2. Menggunakan tata letak jangkar.

3. Komponen - bagilah dan taklukkan.

4. Pemisahan antarmuka dan logika pengguna.

# 1: Menerapkan antarmuka pengguna dalam kode


Di Badoo, sebagian besar minat pengguna diimplementasikan dalam kode. Mengapa kita tidak menggunakan XIB atau storyboard? Pertanyaan yang adil. Alasan utamanya adalah kemudahan mempertahankan kode untuk tim menengah, yaitu:

  • perubahan kode terlihat jelas, yang berarti bahwa tidak perlu mengurai file storyboard / XIB XML untuk menemukan perubahan yang dilakukan oleh seorang rekan;
  • sistem kontrol versi (misalnya, Git) jauh lebih mudah untuk bekerja dengan kode daripada dengan file XLM "berat", terutama selama konflik sedang; itu juga diperhitungkan bahwa isi file XIB / storyboard berubah setiap kali mereka disimpan, bahkan jika antarmuka tidak berubah (meskipun saya mendengar bahwa di Xcode 9 masalah ini telah diperbaiki);
  • mungkin sulit untuk mengubah dan memelihara beberapa properti di Interface Builder (IB), misalnya, properti CALayer selama proses relayout dari tampilan anak (tata letak tampilan), yang dapat menyebabkan beberapa sumber kebenaran untuk kondisi tampilan;
  • Pembuat Antarmuka bukanlah alat tercepat, dan terkadang jauh lebih cepat untuk bekerja secara langsung dengan kode.

Lihatlah pengontrol berikut (FriendsListViewController):

 final class FriendsListViewController: UIViewController { struct ViewConfig { let backgroundColor: UIColor let cornerRadius: CGFloat } private var infoView: FriendsListView! private let viewModel: FriendsListViewModelProtocol private let viewConfig: ViewConfig init(viewModel: FriendsListViewModelProtocol, viewConfig: ViewConfig) { self.viewModel = viewModel self.viewConfig = viewConfig super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.setupContainerView() } private func setupContainerView() { self.view.backgroundColor = self.viewConfig.backgroundColor let infoView = FriendsListView( frame: .zero, viewModel: self.viewModel, viewConfig: .defaultConfig) infoView.backgroundColor = self.viewConfig.backgroundColor self.view.addSubview(infoView) self.infoView = infoView infoView.translatesAutoresizingMaskIntoConstraints = false infoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true infoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true infoView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true infoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true } // …. } 

Contoh ini menunjukkan bahwa Anda dapat membuat view controller hanya dengan menyediakan model tampilan dan konfigurasi tampilan. Anda dapat membaca lebih lanjut tentang model presentasi, yaitu model desain MVVM (Model-View-ViewModel) di sini . Karena konfigurasi tampilan adalah entitas struktural sederhana (struct entitas) yang mendefinisikan tata letak dan gaya tampilan, yaitu indentasi, ukuran, warna, font, dll., Saya menganggap pantas untuk memberikan konfigurasi standar seperti ini:

 extension FriendsListViewController.ViewConfig {   static var defaultConfig: FriendsListViewController.ViewConfig {       return FriendsListViewController.ViewConfig(backgroundColor: .white,                                                   cornerRadius: 16)   } } 

Semua inisialisasi tampilan terjadi dalam metode setupContainerView , yang dipanggil hanya sekali dari viewDidLoad ketika tampilan sudah dibuat dan dimuat, tetapi belum digambar di layar, yaitu, semua elemen yang diperlukan (subview) hanya ditambahkan ke hierarki tampilan, dan kemudian markup diterapkan (tata letak) dan gaya.

Beginilah tampilan pengontrol tampilan sekarang:

 final class FriendsListPresenter: FriendsListPresenterProtocol {   // …   func presentFriendsList(from presentingViewController: UIViewController) {       let controller = Class.createFriendsListViewController( presentingViewController: presentingViewController,           headerViewModel: self.headerViewModel,           contentViewModel: self.contentViewModel)       controller.modalPresentationStyle = .overCurrentContext       controller.modalTransitionStyle = .crossDissolve       presentingViewController.present(controller, animated: true, completion: nil)   }   private class func createFriendsListViewController( presentingViewController: UIViewController, headerViewModel: FriendsListHeaderViewModelProtocol,       contentViewModel: FriendsListContentViewModelProtocol) -> FriendsListContainerViewController {      let dismissViewControllerBlock: VoidBlock = { [weak presentingViewController] in           presentingViewController?.dismiss(animated: true, completion: nil)       }       let infoViewModel = FriendsListViewModel( headerViewModel: headerViewModel,           contentViewModel: contentViewModel)       let containerViewModel = FriendsListContainerViewModel(onOutsideContentTapAction: dismissViewControllerBlock)       let friendsListViewController = FriendsListViewController( viewModel: infoViewModel, viewConfig: .defaultConfig)       let controller = FriendsListContainerViewController( contentViewController: friendsListViewController,           viewModel: containerViewModel,           viewConfig: .defaultConfig)       return controller   } } 

Anda dapat melihat pemisahan tanggung jawab yang jelas, dan konsep ini tidak jauh lebih rumit daripada memanggil segue di papan cerita.

Membuat pengontrol tampilan cukup sederhana, mengingat kami memiliki modelnya dan Anda cukup menggunakan konfigurasi tampilan standar:

 let friendsListViewController = FriendsListViewController( viewModel: infoViewModel, viewConfig: .defaultConfig) 

# 2: Menggunakan tata letak jangkar


Berikut adalah kode tata letaknya:

 self.view.addSubview(infoView) self.infoView = infoView infoView.translatesAutoresizingMaskIntoConstraints = false infoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true infoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true infoView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true infoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true 

Sederhananya, kode ini menempatkan infoView di dalam tampilan induk (superview), pada koordinat (0, 0) relatif terhadap ukuran asli superview.

Mengapa kita menggunakan tata letak jangkar? Cepat dan mudah. Tentu saja, Anda dapat mengatur UIView.frame secara manual dan menghitung semua posisi dan ukuran dengan cepat, tetapi kadang-kadang bisa berubah menjadi terlalu membingungkan dan / atau kode besar.

Anda juga dapat menggunakan format teks untuk markup, seperti yang dijelaskan di sini , tetapi sering kali ini menyebabkan kesalahan, karena Anda harus mengikuti format, dan Xcode tidak memeriksa teks deskripsi markup pada tahap penulisan / kompilasi kode, dan Anda tidak dapat menggunakan Panduan Tata Letak Area Aman:

 NSLayoutConstraint.constraints( withVisualFormat: "V:|-(\(topSpace))-[headerView(headerHeight@200)]-[collectionView(collectionViewHeight@990)]|",   options: [],   metrics: metrics,   views: views) 

Cukup mudah untuk membuat kesalahan atau kesalahan ketik pada string teks yang mendefinisikan markup, bukan?

# 3: Komponen - Bagilah dan Taklukkan


Contoh antarmuka pengguna kami dibagi menjadi beberapa komponen, yang masing-masing menjalankan satu fungsi spesifik, tidak lebih.

Sebagai contoh:

  1. FriendsListHeaderView - Menampilkan informasi tentang teman dan tombol Tutup.
  2. FriendsListContentView - menampilkan daftar teman dengan sel yang dapat diklik, konten dimuat secara dinamis ketika mencapai akhir daftar.
  3. FriendsListView - wadah untuk dua tampilan sebelumnya.

Seperti yang disebutkan sebelumnya, kami di Badoo menyukai prinsip tanggung jawab tunggal ketika setiap komponen bertanggung jawab atas fungsi yang terpisah. Ini membantu tidak hanya dalam proses perbaikan bug (yang, mungkin, bukan bagian paling menarik dari pekerjaan pengembang iOS), tetapi juga selama pengembangan fungsionalitas baru, karena pendekatan ini secara signifikan memperluas kemungkinan penggunaan kembali kode di masa mendatang.

# 4: Memisahkan antarmuka pengguna dan logika


Dan yang terakhir, tetapi yang tidak kalah penting adalah pemisahan antarmuka pengguna dan logika. Teknik yang dapat menghemat waktu dan saraf untuk tim Anda. Dalam arti literal: proyek terpisah untuk antarmuka pengguna dan proyek terpisah untuk logika bisnis.

Mari kita kembali ke contoh kita. Seperti yang Anda ingat, inti dari presentasi (presenter) terlihat seperti ini:

 func presentFriendsList(from presentingViewController: UIViewController) {   let controller = Class.createFriendsListViewController( presentingViewController: presentingViewController,       headerViewModel: self.headerViewModel,       contentViewModel: self.contentViewModel)   controller.modalPresentationStyle = .overCurrentContext   controller.modalTransitionStyle = .crossDissolve   presentingViewController.present(controller, animated: true, completion: nil) } 

Anda hanya perlu memberikan model tampilan untuk judul dan konten. Sisanya disembunyikan di dalam implementasi komponen UI di atas.

Protokol model tampilan tajuk terlihat seperti ini:

 protocol FriendsListHeaderViewModelProtocol {   var friendsCountIcon: UIImage? { get }   var closeButtonIcon: UIImage? { get }   var friendsCount: Observable<String> { get }   var onCloseAction: VoidBlock? { get set } } 

Sekarang bayangkan Anda menambahkan tes visual untuk UI - semudah melewati model rintisan untuk komponen UI.

 final class FriendsListHeaderDemoViewModel: FriendsListHeaderViewModelProtocol {   var friendsCountIcon: UIImage? = UIImage(named: "ic_friends_count")   var closeButtonIcon: UIImage? = UIImage(named: "ic_close_cross")   var friendsCount: Observable<String>   var onCloseAction: VoidBlock?   init() {       let friendsCountString = "\(Int.random(min: 1, max: 5000))"       self.friendsCount = Observable(friendsCountString)   } } 

Itu terlihat sederhana, bukan? Sekarang kami ingin menambahkan logika bisnis ke komponen aplikasi kami, yang mungkin memerlukan penyedia data, model data, dll .:

 final class FriendsListHeaderViewModel: FriendsListHeaderViewModelProtocol {   let friendsCountIcon: UIImage?   let closeButtonIcon: UIImage?   let friendsCount: Observable<String> = Observable("0")   var onCloseAction: VoidBlock?   private let dataProvider: FriendsListDataProviderProtocol   private var observers: [ObserverProtocol] = []   init(dataProvider: FriendsListDataProviderProtocol,        friendsCountIcon: UIImage?,        closeButtonIcon: UIImage?) {       self.dataProvider = dataProvider       self.friendsCountIcon = friendsCountIcon       self.closeButtonIcon = closeButtonIcon       self.setupDataObservers()   }   private func setupDataObservers() {       self.observers.append(self.dataProvider.totalItemsCount.observeNewAndCall { [weak self] (newCount) in           self?.friendsCount.value = "\(newCount)"       })   } } 

Apa yang bisa lebih mudah? Cukup terapkan penyedia data - dan pergi!

Implementasi model konten terlihat sedikit lebih rumit, tetapi pemisahan tanggung jawab masih sangat menyederhanakan kehidupan. Berikut adalah contoh cara instantiate dan menampilkan daftar teman di klik tombol:

 private func presentRealFriendsList(sender: Any) {   let avatarPlaceholderImage = UIImage(named: "avatar-placeholder")   let itemFactory = FriendsListItemFactory(avatarPlaceholderImage: avatarPlaceholderImage)   let dataProvider = FriendsListDataProvider(itemFactory: itemFactory)   let viewModelFactory = FriendsListViewModelFactory(dataProvider: dataProvider)   var headerViewModel = viewModelFactory.makeHeaderViewModel()   headerViewModel.onCloseAction = { [weak self] in       self?.dismiss(animated: true, completion: nil)   }   let contentViewModel = viewModelFactory.makeContentViewModel()   let presenter = FriendsListPresenter( headerViewModel: headerViewModel,       contentViewModel: contentViewModel)   presenter.presentFriendsList(from: self) } 

Teknik ini membantu mengisolasi antarmuka pengguna dari logika bisnis. Selain itu, ini memungkinkan Anda untuk menutupi seluruh UI dengan tes visual, meneruskan data uji ke komponen! Oleh karena itu, pemisahan antarmuka pengguna dan logika bisnis terkait sangat penting untuk keberhasilan proyek, apakah itu startup atau produk yang sudah jadi.

Kesimpulan


Tentu saja, ini hanya beberapa teknik yang digunakan di Badoo, dan itu bukan solusi universal untuk semua kasus yang mungkin. Oleh karena itu, gunakan setelah mengevaluasi apakah cocok untuk Anda dan proyek Anda.

Ada metode lain, misalnya, komponen UI yang dapat dikonfigurasi XIB menggunakan Interface Builder (dijelaskan di artikel kami yang lain), tetapi karena berbagai alasan mereka tidak digunakan di Badoo. Ingat bahwa setiap orang memiliki pendapat dan visi mereka sendiri tentang gambaran besar, oleh karena itu, untuk mengembangkan proyek yang sukses, Anda harus mencapai konsensus dalam tim dan memilih pendekatan yang paling cocok untuk sebagian besar skenario.

Semoga Swift bersamamu!

Sumber

Source: https://habr.com/ru/post/id421559/


All Articles