
Dalam rangkaian artikel ini, saya akan memperkenalkan konsep dasar arsitektur aplikasi dan penerapannya pada aplikasi yang ditulis dalam SwiftUI.
Untuk memulai seri ini, saya ingin memulai dengan sesuatu yang kecil: aplikasi pembaca feed JSON di SwiftUI. Saya ingin fokus pada urutan langkah, bukan pada kode itu sendiri, dan menjawab pertanyaan umum: Mana yang harus Anda tulis terlebih dahulu, kode tampilan atau kode model?
Jawaban singkatnya adalah “keduanya” (atau “tidak keduanya”), namun menjawab pertanyaan ini dengan benar akan membawa pada pertanyaan yang lebih penting: Ketika saya tidak yakin dengan apa yang saya lakukan atau menghalangi, sebagai seorang programmer, Bagaimana saya bisa membuat kemajuan yang dapat diandalkan dalam pekerjaan yang tampaknya terlalu besar untuk dikelola?
Di mana memulainya?
Aplikasi yang ingin saya tulis adalah pembaca feed JSON untuk Kakao dengan feed Love JSON. Aplikasinya akan menjadi aplikasi macOS/iOS SwiftUI seperti ini:

Tujuan akhir dari aplikasi CwlFeedReader
Anda harus memulai dari suatu tempat. Di mana Anda memulai? Model? melihat?
Aplikasi terdiri dari beberapa komponen (data, model, tampilan), dan semua komponen ini bergantung satu sama lain, sehingga menimbulkan masalah ayam-dan-telur tentang komponen mana yang harus ditulis terlebih dahulu.
Tidak masalah dari mana Anda memulai. Yang penting Anda bekerja selangkah demi selangkah (Ulangi) dan memastikan bahwa perubahan segera diterapkan ke setiap komponen utama dalam aplikasi (Mengintegrasikan). Antara iterasi dan integrasi, hal terpenting adalah integrasi – itulah cara Anda mengidentifikasi masalah dan memastikan Anda bergerak ke arah yang benar. Iterasi hanyalah menulis blok kode kecil sehingga Anda dapat kembali ke integrasi lagi.
Saya akan mengikuti pendekatan ini dan mengambil beberapa langkah yang cukup spesifik:
- pengganti
- rintisan
- melaksanakan
- Ulangi
pengganti
Jika kita menginginkan integrasi sejak dini, maka kita memerlukan sesuatu yang dapat menjadi model dan pandangan sejak dini. Ini tidak boleh berupa model nyata atau tampilan nyata, juga tidak boleh memiliki properti substansial dari lapisan nyata. Itu hanya sebuah nama.
Ini adalah konsep placeholder.
Lihat placeholder
Buat aplikasi dari templat proyek SwiftUI Xcode dan Anda akan mendapatkannya ContentView
Ini terlihat seperti ini:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
Ini adalah tampilan placeholder kami. Ia tidak memiliki perilaku dan strukturnya salah, namun ia memiliki nama dan hanya itu yang kita perlukan.
Jawab pertanyaan “Mana yang harus Anda tulis terlebih dahulu, kode tampilan atau kode model?”:melihat Muncul Pertama, tapi saya tidak menulisnya. Bukan jawaban yang paling membantu.
pengganti model
Mari buat placeholder untuk model tersebut. Model untuk aplikasi umpan JSON pada awalnya mungkin hanya berupa umpan JSON itu sendiri.
Ini saja berfungsi sebagai pengganti model – ia mempunyai nama.
Saya pikir saya bisa mengambil setengah langkah ekstra untuk memberikan kedatangan array, karena bagian penting dari feed JSON adalah berisi array artikel.
let articles = ["one", "two", "three"]
susunan ini articles
Bukan representasi feed JSON, namun dapat digunakan sebagai pengganti. Saya memastikan ia memiliki “kardinalitas” yang setara (ini adalah array, bukan objek tunggal), yang akan membantu saya melanjutkan ke iterasi berikutnya.
Jawab pertanyaan “Mana yang harus Anda tulis terlebih dahulu, kode tampilan atau kode model?”: Baris kode pertama yang saya tulis adalah model placeholder, tetapi tidak bertahan pada iterasi berikutnya. Mungkin ini bukan pertanyaan yang paling berguna.
Integrasi tempat penampung
Sebelum melakukan iterasi lebih jauh, kita memerlukan integrasi – yang tidak lebih dari menampilkan model.
struct ContentView: View {
let articles = ["one", "two", "three"]
var body: some View {
Text("Hello, world!").padding()
}
}
Integrasi ini sangat sederhana. Kita akan melihat integrasi yang lebih substansial di bagian berikutnya sebelum memilih urutan perubahan dan membuat perubahan model sebelum melihat perubahan.
rintisan
tampilan rintisan
Ini adalah awal dari iterasi kedua, artinya membuang kode dari iterasi yang lama. Dalam hal ini kami membuang seluruh isinya ContentView.body
milik.
Sebagai gantinya kita bisa mematikan List
gunakan susunan articles
.
struct ContentView: View {
let articles = ["one", "two", "three"]
var body: some View {
NavigationView {
List(articles, id: \.self) { article in
NavigationLink(destination: Text(article)) {
Text(article)
}
}
Color.clear
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
Tampilan rintisan kami memiliki komponen dasar: DoubleColumnNavigationViewStyle
Dan List
dan sebuah NavigationLink
Namun pandangan-pandangan ini tidak mempunyai isi aktual dan tidak mempunyai tujuan.
Menghapus tampilan mungkin merupakan aspek terbaik dari SwiftUI. Dalam 14 baris kode (5 di antaranya sebagian besar kosong) Anda dapat melihat keseluruhan program.
Kode ini berjalan dan memberikan aplikasi berikut:
Saat kami menambahkan peningkatan dan fitur ke SwiftUI, detailnya dapat membuat struktur dasarnya menjadi kerdil. Menyembunyikan detail tanpa menyembunyikan struktur harus menjadi tujuan pengembang SwiftUI yang baik. Penting untuk mengingat seperti apa tampilan Anda pada tahap stub, dan terus membangun kembali untuk merender ulang struktur dasar ini.
model rintisan
Seperti yang kami lakukan saat memulai tampilan rintisan, kami akan membuangnya semua Model rintisan. Itu hanya sebuah nama, tidak ada yang bisa diwariskan (kami bahkan tidak akan menyimpan nama itu).
Apa yang kita inginkan? Persyaratan utama model stub adalah model tersebut harus berada dalam struktur data yang diharapkan, jadi kami akan menambahkan blok kode yang lebih besar di sini.
ini Feed
Dan Article
Jenisnya ditentukan oleh format standar JSONFeed, sehingga tidak sulit untuk menulisnya.
struct Feed: Codable {
let items: [Article]
}
struct Article: Codable {
let url: URL
let title: String
let content: String
enum CodingKeys: String, CodingKey {
case url, title, content = "content_html"
}
}
class Model: ObservableObject {
@Published var feed: Feed? = Feed(
items: [
Article(url: URL(string: "mock://x/a1.html")!, title: "Article1", content: "One"),
Article(url: URL(string: "mock://x/a2.html")!, title: "Article2", content: "Two"),
Article(url: URL(string: "mock://x/a3.html")!, title: "Article3", content: "Three")
]
)
}
integrasi rintisan
Dalam kode di atas, saya melakukannya Model
sesuai dengan ObservableObject
dan menyediakan feed
sebagai @Published
milik. Ini adalah persyaratan teknis untuk menghubungkan versi model ini ke tampilan.
Di ujung lain tautan, kita harus menggunakan ContentView
Lihat ini baru Model
. kami menghapus articles
rintisan, tapi malah berisi yang baru model
.
struct ContentView: View {
@ObservedObject var model: Model
// ...
}
semua ContentView
Konstruksi sekarang harus diselesaikan Model
Dan List
Perlu diperbarui untuk mengikuti struktur profil baru.
List(model.feed?.items ?? [], id: \.url) { row in
NavigationLink(destination: Text(row.content)) {
Text(row.title)
}
}
Aplikasi yang sekarang berjalan menunjukkan model rintisan kami terlihat di tampilan:
Realisasi fungsi
Implementasi fungsi model
Kita sekarang berada pada saat seperti ini: Model
Muat umpan sebenarnya. Sejak implementasi ini Model
antarmuka, tidak masalah apakah langkah ini terjadi sebelum atau sesudah perubahan serupa pada tampilan.
Konstruk placeholder dari feed
dibuang. Sebagai gantinya kami akan menggunakan metode berikut untuk mendapatkan data sebenarnya URLRequest
:
var task: URLSessionDataTask?
init() {
let request = URLRequest(url: URL(string: "https://www.cocoawithlove.com/feed.json")!)
task = URLSession.shared.dataTask(with: request) { data, response, error in
do {
if let error = error { throw error }
let feed = try JSONDecoder().decode(Feed.self, from: data ?? Data())
DispatchQueue.main.async { self.feed = feed }
} catch {}
}
task?.resume()
}
Lihat implementasi fungsi
Tampilan yang setara dengan implementasi fitur dalam model adalah perubahan tampilan yang tidak bergantung pada materi model. Sekali lagi, seperti halnya implementasi fitur model, hal ini mungkin terjadi sebelum atau sesudah perubahan model.
Dalam implementasi ini, lebih sedikit kode yang dibuang dan lebih banyak proses pemfaktoran ulang bertahap. Saya menambahkan judul bilah navigasi baru, gaya bilah navigasi, dan UIViewRepresentable
-dibungkus WKWebView
untuk konten feed, bukan sekadar Text
melihat). ini ContentView
body
Pembaruan fungsinya adalah sebagai berikut:
func detailView(_ row: Article) -> some View {
SharedWebView(content: row.content)
.navigationTitle(Text(row.title))
.navigationBarTitleDisplayModeIfAvailable(.inline)
}
var body: some View {
NavigationView {
List(model.feed?.items ?? [], id: \.url) { row in
NavigationLink(destination: detailView(row)) {
Text(row.title)
}
}
.navigationTitle(Text("Articles"))
.navigationBarTitleDisplayModeIfAvailable(.inline)
Color.clear
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
Ini terlihat seperti ini:
Pada titik ini, kami memiliki versi aplikasi yang fungsional ujung ke ujung. Dengan berfokus pada satu jalur data (feed), kami menghindari kompleksitas dan kini dapat membangun inti dengan menambahkan tombol tambahan dan perubahan visual pada desain target.
Implementasi berulang
Saya ingin setiap artikel memiliki status “dibaca”, dan saya ingin artikel tersebut tetap tidak berubah. Sekarang mari kita tambahkan ini.
Di bagian placeholder dan stub, saya menulis tampilan sebelum model, tetapi pada setiap tahap integrasi yang diperlukan setelah perubahan model meningkat. Sekarang kita memiliki tampilan yang tidak kosong, lebih mudah untuk memulai perubahan pada model, sehingga kita dapat menggabungkan pembaruan pada tampilan dengan langkah integrasi apa pun yang diperlukan.
Saat Anda mengisi struktur dan pola program Anda, menangani perubahan yang lebih kompleks di setiap langkah menjadi lebih mudah. Kuncinya adalah melihat apakah Anda dapat terus menulis kode tanpa kehilangan fokus, mengalami kesalahan, atau mundur. Jika Anda melihat masalah ini terjadi, itu tandanya Anda harus mencoba mengulanginya dalam langkah-langkah yang lebih kecil.
Iterasi model
Dalam hal ini, saya akan menambahkan status “baca” dalam satu langkah. Implementasi “sudah dibaca” Model
Ini terlihat seperti ini:
@Published var isReadStatuses: [URL: Bool]
init() {
self.isReadStatuses = UserDefaults.standard.data(forKey: "isReadStatuses")
.flatMap { try? JSONDecoder().decode([URL: Bool].self, from: $0) } ?? [:]
// ... feed loading omitted
}
func setIsRead(_ value: Bool, url: URL) {
isReadStatuses[url] = value
UserDefaults.standard.set(try? JSONEncoder().encode(isReadStatuses), forKey: "isReadStatuses")
}
Lihat iterasi
Perubahan model terbaru ini bisa saja berubah detailView
Setelah rendering (dengan mengubah status “baca”). untuk a NavigationView
Untuk berubah, ia harus mengamati model itu sendiri (tidak bisa bergantung pada pengamatan orang tua, jika tidak, keadaan lama akan tetap ada ketika keadaan berubah), jadi kita perlu pindah detailView
menjadi milik sendiri View
melaksanakan:
struct DetailView: View {
@ObservedObject var model: Model
let article: Article
var body: some View {
let isRead = model.isReadStatuses[article.url] ?? false
return SharedWebView(content: article.content)
.navigationTitle(Text(article.title))
.navigationBarItems(
trailing:
Button {
model.setIsRead(!isRead, url: article.url)
} label: {
Text(isRead ? "Mark as unread" : "Mark as read")
}
)
.navigationBarTitleDisplayMode(.inline)
.onAppear { model.setIsRead(true, url: article.url) }
}
}
Dan gunakan implementasi baru ini di kolom utama:
List(rows, id: \.url) { row in
NavigationLink(destination: DetailView(model: model, article: row)) {
HStack {
let isRead = model.isReadStatuses[row.url] ?? false
Image(systemName: isRead ? "checkmark.circle" : "circle")
Text(row.title)
}
}
}
iterasi lebih lanjut
Seperti yang dapat Anda lihat dari iterasi ini, kami telah beralih dari iterasi yang membuang kode ke pengembangan fitur yang lebih sederhana yang menambahkan kode baru namun menghapus sangat sedikit. Tidak apa-apa; ini tandanya inti sudah selesai.
Dengan cara ini, fungsionalitas berikut ditambahkan ke kode akhir:
- Penanganan kesalahan
- pengisian ulang umpan
- Ganti status “baca” setiap artikel secara manual
Kami telah mencapai status target aplikasi kami:
Bukankah ini semua sudah jelas?
Setelah membaca apa yang saya katakan, saya merasa seperti saya tidak mengatakan apa pun yang tidak sepenuhnya jelas. Tulis beberapa baris kode, klik Bangun dan Jalankan, konfirmasikan bahwa kode tersebut dijalankan, lalu tambahkan kode lainnya. Itu yang dilakukan semua orang saat memprogram, bukan?
Meskipun jelas, bekerja dengan iterasi yang ketat adalah salah satu aturan terbaik yang sering kali gagal saya ikuti.
Jika saya sangat disiplin, saya akan menggabungkan kode dalam proyek saya sekitar dua kali sehari, dan saya akan menggabungkan antara 100 dan 400 baris setiap kali. Ketika frekuensi ini dipertahankan, permintaan penarikan dapat dikelola dan dibaca, anggota tim lain dapat mengomentari arah sebelum menulis terlalu banyak kode, dan cerita berkembang dengan kecepatan yang dapat diprediksi.
Sayangnya, kita mudah memikirkan gambaran besarnya dan secara tidak sengaja mulai menangani semuanya sekaligus. Unit kode yang lebih besar berjalan lebih lambat. Ini bukan pertumbuhan eksponensial, namun lebih buruk daripada pertumbuhan linier – mengintegrasikan 2000 baris perubahan biasanya memerlukan waktu 10 kali lebih lama dibandingkan mengintegrasikan 200 baris perubahan. Perubahan kode yang lebih besar juga lebih sulit untuk dipikirkan – anggota tim yang meninjau perubahan Anda cenderung melepaskan diri secara mental daripada memberikan umpan balik yang berguna. Dan kesalahan cenderung menjadi lebih besar dan lebih sulit untuk diperbaiki, dan kekeliruan biaya hangus (sunk cost fallacy) lebih sering terjadi seiring dengan bertambahnya waktu yang diinvestasikan.
Jika iterasi dan integrasi kecil “jelas”, mengapa hal tersebut tidak terjadi secara alami dalam proyek nyata?
Kesulitan muncul ketika Anda tidak menerapkan “iterasi” dan “integrasi” yang jelas seperti yang Anda lakukan saat memfaktorkan ulang satu jenis kode ke jenis kode lainnya. Menemukan langkah-langkah kecil dan jelas menjadi sebuah tantangan. Fase “Placeholder”, “Stub”, “Implementasi Fitur”, dan “Iterasi Lebih Lanjut” menjadi lebih membingungkan.
Beberapa opsi dapat membantu…
implementasi paralel
Daripada menghapus 5.000 baris kode dalam satu blok, mulailah implementasi baru yang independen di samping implementasi lama dan pindahkan elemen dari implementasi lama ke implementasi baru, satu per satu. Dengan cara ini Anda dapat menerapkan pemfaktoran ulang besar-besaran seolah-olah Anda sedang membuat kode baru yang bersih. Kode dan gunakan implementasi baru untuk komponen yang sudah dimigrasi, namun gunakan implementasi lama untuk komponen yang belum dimigrasi.
Peralihan fungsi
Jika Anda tidak dapat mengirimkan blok kode baru karena tidak berfungsi, Anda selalu dapat mematikannya melalui fitur pengalih (kondisi waktu pembuatan atau waktu proses dalam kode Anda yang melewatkan kode baru). Hal ini memungkinkan Anda mengirimkan kode yang tidak lengkap atau tidak berfungsi ke repositori, mendapatkan masukan, dan melacak kemajuan. Ini tidak sebaik implementasi paralel (yang dapat digunakan sebagian dari awal), namun tetap dapat membuat Anda tetap termotivasi.
isolasi bangunan
Pilihan terbaik untuk menjaga perubahan kode tetap kecil adalah dengan memiliki basis kode di mana semua definisi dan fungsi diisolasi dan tidak ada definisi khusus aplikasi yang mencakup ribuan baris kode.
Pendekatan ini menjadi sulit ketika aplikasi Anda memiliki puluhan atau bahkan ratusan ribu baris kode, namun saya akan membahas topik ini lebih lanjut di artikel berikutnya dalam seri ini.
Apa yang harus dilakukan selanjutnya?
Pada artikel ini, saya menjawab pertanyaan “Mana yang harus Anda tulis terlebih dahulu, kode tampilan atau kode model?” dan menjawab dengan lantang, “Mungkin itu bukan pertanyaan yang paling berguna.”
Pada artikel selanjutnya saya ingin menjawab pertanyaan berikut: Pola desain aplikasi apa yang saya gunakan di sini? Saya berjanji jawabannya akan lebih mudah.
Kode untuk artikel ini tersedia di github, dan setiap tahap iterasi tersedia sebagai penerapan terpisah dalam riwayat.