Permintaan HTTP
adalah salah satu keterampilan paling penting yang perlu Anda dapatkan ketika mengembangkan iOS
aplikasi. Dalam versi sebelumnya Swift
(hingga versi 5), terlepas dari apakah Anda membuat permintaan ini "dari awal" atau menggunakan kerangka Alamofire yang terkenal , Anda berakhir dengan kode callback
jenis yang kompleks dan membingungkan completionHandler: @escaping(Result<T, APIError>) -> Void
.Penampilan dalam Swift 5
kerangka baru pemrograman reaktif fungsional Combine
dalam hubungannya dengan yang ada URLSession
, dan Codable
memberi Anda semua alat yang diperlukan untuk menulis kode yang sangat kompak secara independen untuk mengambil data dari Internet.Dalam artikel ini, sesuai dengan konsepnya, Combine
kami akan membuat "penerbit"Publisher
untuk memilih data dari Internet, yang dapat dengan mudah kita “berlangganan” di masa depan dan digunakan saat mendesain UI
dengan UIKit
dan dengan bantuan SwiftUI
.Karena SwiftUI
terlihat lebih ringkas dan lebih efektif, karena tindakan "penerbit" Publisher
tidak terbatas hanya pada data sampel, dan meluas lebih jauh hingga ke kontrol antarmuka pengguna ( UI
). Faktanya adalah bahwa SwiftUI
pemisahan data View
dilakukan menggunakan ObservableObject
kelas dengan @Published
properti, perubahan yang secara otomatis SwiftUI
dipantau dan sepenuhnya digambar ulang View
.Di ObservableObject
kelas - kelas ini , Anda dapat dengan mudah menempatkan logika bisnis tertentu dari aplikasi, jika beberapa di antaranya@Published
properti adalah hasil dari sinkron dan / atau asynchronous transformasi lainnya @Published
sifat yang dapat langsung diubah "aktif" seperti elemen antarmuka pengguna ( UI
) kotak teks TextField
, Picker
, Stepper
, Toggle
dllUntuk memperjelas apa yang dipertaruhkan, saya akan memberikan contoh spesifik. Sekarang banyak layanan seperti NewsAPI.org dan Hacker News menawarkan agregator berita untuk menawarkan kepada pengguna untuk memilih set artikel yang berbeda tergantung pada minat mereka. Dalam kasus agregator berita NewsAPI.org, ini bisa berupa berita terbaru, atau berita dalam beberapa kategori - "olahraga", "kesehatan", "sains", "teknologi", "bisnis", atau berita dari sumber informasi spesifik "CNN" , Berita ABC, Bloomberg, dll. Pengguna biasanya "mengekspresikan" keinginannya untuk layanan dalam bentuk Endpoint
yang membentuk kebutuhan untuknya URL
.Jadi, menggunakan framework Combine
, Anda bisaObservableObject
kelas menggunakan kode yang sangat kompak (dalam kebanyakan kasus tidak lebih dari 10-12 baris) sekali untuk membentuk ketergantungan sinkron dan / atau tidak sinkron dari daftar artikel Endpoint
dalam bentuk "berlangganan" dari @Published
properti "pasif" ke properti "aktif" @Published
. "Langganan" ini akan valid sepanjang seluruh "siklus hidup" instance ObservableObject
kelas. Dan kemudian di dalam SwiftUI
Anda akan memberikan pengguna kesempatan untuk mengelola hanya properti "aktif" @Published
dalam bentuk Endpoint
, yaitu, APA yang ingin ia lihat: apakah itu akan menjadi artikel dengan berita terbaru atau artikel di bagian "kesehatan". Penampilan artikel-artikel itu sendiri dengan berita-berita terbaru atau artikel-artikel di bagian "kesehatan" pada Anda UI
akan diberikan secara OTOMATIS oleh ObservableObject
kelas - kelas ini dan properti "pasif" @Published. Dalam kodeSwiftUI
Anda tidak akan pernah perlu secara eksplisit meminta pilihan artikel, karena ObservableObject
kelas yang memainkan peran bertanggung jawab atas tampilan yang benar dan sinkron di layar View Model
.Saya akan menunjukkan cara kerjanya dengan NewsAPI.org dan Hacker News dan basis data film TMDb dalam serangkaian artikel. Dalam ketiga kasus, kira-kira pola penggunaan yang sama akan beroperasi Combine
, karena dalam aplikasi semacam ini Anda selalu harus membuat LISTS film atau artikel, pilih "GAMBAR" (gambar) yang menyertai mereka, CARI database untuk film atau artikel yang dibutuhkan menggunakan bilah pencarian.Saat mengakses layanan tersebut, kesalahan dapat terjadi, misalnya, karena Anda menetapkan kunci yang salah API-key
atau melebihi jumlah permintaan yang diizinkan atau sesuatu yang lain. Anda perlu menangani kesalahan semacam ini, jika tidak Anda berisiko meninggalkan pengguna sepenuhnya kehilangan layar kosong. Oleh karena itu, Anda harus dapat tidak hanya memilih Combine
data dari Internet menggunakan , tetapi juga melaporkan kesalahan yang mungkin terjadi selama pengambilan sampel, dan mengontrol penampilan mereka di layar.Kami akan mulai mengembangkan strategi kami dengan mengembangkan aplikasi yang berinteraksi dengan agregator berita NewsAPI.org . Saya harus mengatakan bahwa dalam aplikasi ini SwiftUI
akan digunakan seminimal mungkin tanpa embel-embel dan hanya untuk menunjukkan bagaimana Combine
dengan "penerbit" nyaPublisher
dan "berlangganan" Subscription
terpengaruh UI
.Disarankan agar Anda mendaftar di situs web NewsAPI.org dan menerima kunci API
yang diperlukan untuk menyelesaikan segala permintaan ke layanan NewsAPI.org . Anda harus meletakkannya di file NewsAPI.swift dalam struktur APIConstants
.Kode aplikasi untuk artikel ini ada di Github .NewsAPI.org Model Data Layanan dan API
Layanan NewsAPI.org memungkinkan Anda memilih informasi tentang artikel berita terkini [Article]
dan sumbernya [Source]
. Model data kami akan sangat sederhana, terletak di Articles.swift berkas :import Foundation
struct NewsResponse: Codable {
let status: String?
let totalResults: Int?
let articles: [Article]
}
struct Article: Codable, Identifiable {
let id = UUID()
let title: String
let description: String?
let author: String?
let urlToImage: String?
let publishedAt: Date?
let source: Source
}
struct SourcesResponse: Codable {
let status: String
let sources: [Source]
}
struct Source: Codable,Identifiable {
let id: String?
let name: String?
let description: String?
let country: String?
let category: String?
let url: String?
}
Artikel ini Article
akan berisi pengidentifikasi id
, judul title
, deskripsi description
, penulis author
, URL dari "gambar" urlToImage
, tanggal publikasi publishedAt
dan sumber publikasi source
. Di atas artikel [Article]
adalah add- NewsResponse
in di mana kita hanya akan tertarik pada properti articles
, yang merupakan array artikel. Struktur NewsResponse
dan struktur root Article
adalah Codable
, yang akan memungkinkan kita untuk secara harfiah dua baris kode decoding JSON
data ke dalam Model. Struktur Article
juga harus Identifiable
, jika kita ingin membuat lebih mudah untuk diri kita sendiri untuk menampilkan array artikel [Article]
sebagai daftar List
di SwiftUI
. Protokol Identifiable
membutuhkan keberadaan propertiid
yang akan kami berikan dengan pengidentifikasi unik buatan UUID()
.Sumber informasi Source
akan berisi pengidentifikasi id
, nama name
, deskripsi description
, negara country
, kategori sumber publikasi category
, URL situs url
. Di atas sumber informasi [Source]
ada add- SourcesResponse
in di mana kita hanya akan tertarik pada properti sources
, yang merupakan array dari sumber informasi. Struktur SourcesResponse
dan struktur root Source
adalah Codable
, yang akan memungkinkan kita untuk dengan mudah mendekode JSON
data menjadi model. Struktur Source
juga harus Identifiable
, jika kita ingin memfasilitasi tampilan berbagai sumber informasi [Source]
dalam bentuk daftar List
diSwiftUI
. Protokol Identifiable
mensyaratkan keberadaan properti id
yang sudah kita miliki, jadi tidak ada upaya tambahan yang diperlukan dari kami.Sekarang pertimbangkan apa yang kita butuhkan API
untuk layanan NewsAPI.org dan letakkan di file NewsAPI.swift . Bagian utama dari kami API
adalah kelas NewsAPI
yang menyajikan dua metode untuk memilih data dari agregator berita NewsAPI.org - artikel [Article]
dan sumber informasi [Source]
:fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
- pemilihan artikel [Article]
berdasarkan parameter endpoint
,fetchSources (for country: String) -> AnyPublisher<[Source], Never>
- Pilihan sumber informasi [Source]
untuk negara tertentu country
.
Metode-metode ini menghasilkan tidak hanya array artikel [Article]
atau array sumber informasi [Source]
, tetapi "penerbit" yang sesuai dari Publisher
kerangka kerja baru Combine
. Kedua penerbit tidak mengembalikan kesalahan apa pun - Never
dan jika kesalahan pengambilan sampel atau pengkodean masih terjadi, maka array kosong artikel [Article]()
atau sumber informasi dikembalikan [Source]()
tanpa pesan apa pun mengapa array ini kosong. Artikel atau sumber informasi mana yang ingin kami pilih dari server NewsAPI.org , kami akan tunjukkan menggunakan enumerasienum Endpoint:
enum Endpoint {
case topHeadLines
case articlesFromCategory(_ category: String)
case articlesFromSource(_ source: String)
case search (searchFilter: String)
case sources (country: String)
var baseURL:URL {URL(string: "https://newsapi.org/v2/")!}
func path() -> String {
switch self {
case .topHeadLines, .articlesFromCategory:
return "top-headlines"
case .search,.articlesFromSource:
return "everything"
case .sources:
return "sources"
}
}
}
Itu:- berita terbaru
.topHeadLines
, - berita dari kategori tertentu (olahraga, sehat, sains, bisnis, teknologi)
.articlesFromCategory(_ category: String)
, - berita dari sumber informasi tertentu (CNN, ABC News, Fox News, dll.)
.articlesFromSource(_ source: String)
, - berita apa pun
.search (searchFilter: String)
yang memenuhi persyaratan tertentu searchFilter
, - sumber informasi
.sources (country:String)
untuk negara tertentu country
.
Untuk memfasilitasi inisialisasi opsi yang kita butuhkan, kami menambahkan Endpoint
inisialisasi init?
ke enumerasi untuk berbagai daftar artikel dan sumber informasi tergantung pada indeks index
dan string text
, yang memiliki arti berbeda untuk opsi enumerasi yang berbeda:init? (index: Int, text: String = "sports") {
switch index {
case 0: self = .topHeadLines
case 1: self = .search(searchFilter: text)
case 2: self = .articlesFromCategory(text)
case 3: self = .articlesFromSource(text)
case 4: self = .sources (country: text)
default: return nil
}
}
Mari kita kembali ke kelas NewsAPI
dan mempertimbangkan lebih detail metode pertama fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>
, yang memilih artikel [Article]
berdasarkan parameter endpoint
dan tidak mengembalikan kesalahan apa pun - Never
:func fetchArticles(from endpoint: Endpoint) -> AnyPublisher<[Article], Never> {
guard let url = endpoint.absoluteURL else {
return Just([Article]()).eraseToAnyPublisher()
}
return
URLSession.shared.dataTaskPublisher(for:url)
.map{$0.data}
.decode(type: NewsResponse.self,
decoder: APIConstants .jsonDecoder)
.map{$0.articles}
.replaceError(with: [])
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
- atas dasar
endpoint
formulir URL
untuk meminta daftar artikel yang diinginkan endpoint.absoluteURL
, jika ini tidak dapat dilakukan, maka kembalikan array artikel kosong[Article]()
- «»
dataTaskPublisher(for:)
, Output
(data: Data, response: URLResponse)
, Failure
- URLError
, map { }
(data: Data, response: URLResponse)
data
, JSON
data
, NewsResponse
, articles: [Atricle]
map { }
- articles
, - -
[ ]
, main
, UI
,- «» «»
eraseToAnyPublisher()
AnyPublisher
.
Tugas memilih sumber informasi ditugaskan ke metode kedua - fetchSources (for country: String) -> AnyPublisher<[Source], Never>
yang merupakan salinan semantik yang tepat dari metode pertama, kecuali bahwa kali ini alih-alih artikel [Article]
kami akan memilih sumber informasi [Source]
:func fetchSources() -> AnyPublisher<[Source], Never> {
guard let url = Endpoint.sources.absoluteURL else {
return Just([Source]()).eraseToAnyPublisher()
}
return
URLSession.shared.dataTaskPublisher(for:url)
.map{$0.data}
.decode(type: SourcesResponse.self,
decoder: APIConstants .jsonDecoder)
.map{$0.sources}
.replaceError(with: [])
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
Ia mengembalikan kepada kita "penerbit" AnyPublisher <[Source], Never>
dengan nilai dalam bentuk array sumber informasi [Source]
dan tidak adanya kesalahan Never
(jika terjadi kesalahan, array kosong sumber dikembalikan [ ]
).Kami akan memilih bagian umum dari kedua metode ini, mengaturnya sebagai Generic
fungsi fetch(_ url: URL) -> AnyPublisher<T, Error>
yang mengembalikan Generic
"penerbit" AnyPublisher<T, Error>
berdasarkan URL
:
func fetch<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data}
.decode(type: T.self, decoder: APIConstants.jsonDecoder)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
Ini akan menyederhanakan dua metode sebelumnya:
func fetchArticles(from endpoint: Endpoint)
-> AnyPublisher<[Article], Never> {
guard let url = endpoint.absoluteURL else {
return Just([Article]()).eraseToAnyPublisher()
}
return fetch(url)
.map { (response: NewsResponse) -> [Article] in
return response.articles }
.replaceError(with: [Article]())
.eraseToAnyPublisher()
}
func fetchSources(for country: String)
-> AnyPublisher<[Source], Never> {
guard let url = Endpoint.sources(country: country).absoluteURL
else {
return Just([Source]()).eraseToAnyPublisher()
}
return fetch(url)
.map { (response: SourcesResponse) -> [Source] in
response.sources }
.replaceError(with: [Source]())
.eraseToAnyPublisher()
}
"Penerbit" yang diperoleh tidak mengirimkan apa pun sampai seseorang "berlangganan" kepada mereka. Kita bisa melakukan ini ketika mendesain UI
."Penerbit" Publisher
seperti View Model
pada SwiftUI
. Daftar artikel.
Sekarang sedikit tentang logika fungsi SwiftUI
.Satu- SwiftUI
satunya abstraksi dari "perubahan eksternal" yang mereka respons View
adalah "penerbit" Publisher
. "Perubahan eksternal" dapat dipahami sebagai timer Timer
, pemberitahuan dengan NotificationCenter
atau objek Model Anda, yang menggunakan protokol ObservableObject
dapat diubah menjadi "sumber kebenaran" tunggal eksternal (sumber kebenaran). Untuk tipe "penerbit" biasa Timer
atau NotificationCenter
View
bereaksi menggunakan metode onReceive (_: perform:)
. Contoh penggunaan "penerbit" Timer
akan kami sajikan nanti di artikel ketiga tentang pembuatan aplikasi untuk Hacker News .Pada artikel ini, kita akan fokus pada bagaimana membuat Model kita SwiftUI
eksternal "sumber kebenaran" (sumber kebenaran).Mari pertama-tama kita lihat bagaimana SwiftUI
"penerbit" yang dihasilkan harus berfungsi dalam contoh spesifik menampilkan berbagai jenis artikel:.topHeadLines
- berita terbaru, .articlesFromCategory(_ category: String)
- berita untuk kategori tertentu, .articlesFromSource(_ source: String)
- berita untuk sumber informasi tertentu, .search (searchFilter: String)
- berita yang dipilih oleh kondisi tertentu.
Bergantung pada Endpoint
pengguna mana yang dipilih, kita perlu memperbarui daftar artikel yang articles
dipilih dari NewsAPI.org . Untuk melakukan ini, kita akan membuat kelas ArticlesViewModel
yang sangat sederhana yang mengimplementasikan protokol ObservableObject
dengan tiga @Published
properti: 
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , ( «», View
TextField
),@Published var articles: [Article]
- ( «», NewsAPI.org, «»).
Segera setelah kami menetapkan @Published
properti indexEndpoint
atau searchString
, kami dapat mulai menggunakannya baik sebagai properti sederhana indexEndpoint
dan searchString
, dan sebagai "penerbit" $indexEndpoint
dan $searchString
.Di kelas ArticlesViewModel
, Anda tidak hanya dapat mendeklarasikan properti yang menarik bagi kami, tetapi juga meresepkan logika bisnis dari interaksinya. Untuk tujuan ini, ketika menginisialisasi instance kelas ArticlesViewModel
, init
kita dapat membuat "langganan" yang akan bertindak di seluruh "siklus hidup" instance kelas ArticlesViewModel
dan mereproduksi ketergantungan dari daftar artikel articles
pada indeks indexEndpoint
dan string pencarian searchString
.Untuk melakukan ini, Combine
kami memperluas rantai "penerbit" $indexEndpoint
dan $searchString
menghasilkan "penerbit"AnyPublisher<[Article], Never>
yang nilainya adalah daftar artikel articles
. Kemudian kami "berlangganan" menggunakan operator assign (to: \.articles, on: self)
dan mendapatkan daftar artikel yang kami butuhkan articles
sebagai @Published
properti "output" yang menentukan UI
.Kita harus menarik rantai TIDAK hanya dari properti indexEndpoint
dan searchString
, yaitu dari "penerbit" $indexEndpoint
dan $searchString
yang berpartisipasi dalam pembuatan UI
dengan bantuan SwiftUI
dan kami akan mengubahnya di sana menggunakan elemen antarmuka pengguna Picker
dan TextField
.Bagaimana kita akan melakukan ini?Kami sudah memiliki fungsi di gudang senjata kami fetchArticles (from: Endpoint)
yang ada di kelas NewsAPI
dan mengembalikan "penerbit" AnyPublisher<[Article], Never>
, tergantung pada nilainyaEndpoint
, dan kami hanya bisa menggunakan nilai "penerbit" $indexEndpoint
dan $searchString
mengubahnya menjadi argumen untuk endpoint
fungsi ini. Pertama, gabungkan "penerbit" $indexEndpoint
dan $searchString
. Untuk melakukan ini, Combine
operator ada Publishers.CombineLatest
:
Untuk membuat "penerbit" baru berdasarkan data yang diterima dari "penerbit" sebelumnya Combine
, operator digunakan flatMap
:
Selanjutnya, kami "berlangganan" ke "penerbit" yang baru diterima ini menggunakan "pelanggan" yang sangat sederhana assign (to: \.articles, on: self)
dan menetapkan yang diterima dari " penerbit "nilai untuk @Published
array articles
:
Kami baru saja membuat init( )
" penerbit "ASYNCHRONOUS" dan "berlangganan" untuk itu, sebagai akibat dariAnyCancellable
"Berlangganan" dan ini mudah untuk diverifikasi jika kita menjaga "langganan" kita dalam konstanta let subscription
:
Properti utama dari AnyCancellable
"berlangganan" adalah bahwa begitu ia meninggalkan ruang lingkupnya, memori yang ditempati olehnya secara otomatis dibebaskan. Oleh karena itu, segera setelah init( )
selesai, "langganan" ini akan dihapus ARC
, tanpa memiliki waktu untuk menetapkan informasi asinkron yang diterima dengan penundaan waktu ke array articles
. Informasi asinkron tidak memiliki tempat untuk "mendarat", dalam arti harfiahnya, "bumi telah hilang dari kakinya."Untuk menyimpan "langganan" seperti itu, perlu membuat init()
variabel DI LUAR penginisialisasi var cancellableSet
yang akan menyimpan AnyCancellable
"langganan" kami dalam variabel ini di seluruh "siklus hidup" instance kelas ArticlesViewMode
. Oleh karena itu, kami menghapus konstanta let subscription
dan mengingat AnyCancellable
"langganan" kami dalam variabel cancellableSet
menggunakan operator .store ( in: &self.cancellableSet)
:
"Berlangganan" ke "penerbit" ASYNCHRONOUS yang kami buat init( )
akan dipertahankan sepanjang seluruh "siklus hidup" instance kelas ArticlesViewModel
.Kami dapat secara sewenang-wenang mengubah arti "penerbit" $indexEndpoint
dan / atau searchString
, dan selalu berkat "berlangganan" yang dibuat, kami akan memiliki serangkaian artikel yang sesuai dengan nilai-nilai dari dua penerbit articles
tanpa upaya tambahan. Ini ObservableObject
kelas biasanya disebut View Model
.Untuk mengurangi jumlah panggilan ke server saat mengetik string pencarian searchString
, kita tidak boleh menggunakan "penerbit" dari string pencarian itu sendiri $searchString
, dan versinya yang dimodifikasi validString
:
Sekarang kita punya View Model
artikel, mari kita mulai membuat antarmuka pengguna ( UI
). Untuk SwiftUI
menyinkronkan View
dengan ObservableObject
Model, @ObservedObject
variabel digunakan yang merujuk pada instance kelas Model ini. Pasangan ini - ObservableObject
kelas dan @ObservedObject
variabel yang mereferensikan instance kelas ini - yang mengontrol perubahan dalam antarmuka pengguna ( UI
) di SwiftUI
.Kami menambahkan struktur ContentView
instance kelas ArticleViewModel
dalam bentuk variabel var articleViewModel
dan menggantinya Text ("Hello, World!")
dengan daftar artikel ArticlesList
di mana kami menempatkan artikel yang articlesViewModel.articles
diperoleh dari kami View Model
. Akibatnya, kami mendapatkan daftar artikel untuk indeks tetap dan default indexEndpoint = 0
, yaitu, untuk .topHeadLines
berita terbaru:
Tambahkan UI
elemen ke layar kami untuk mengontrol set artikel mana yang ingin kami tampilkan. Kami akan menggunakan Picker
perubahan indeks $articlesViewModel.indexEndpoint
. Kehadiran simbol adalah $
wajib, karena ini berarti perubahan dalam nilai yang diberikan oleh @Published
"penerbit". "Berlangganan" ke "penerbit" ini dipicu segera, yang kami mulai init ()
, "output" @Published
"penerbit" articles
akan berubah dan kami akan melihat daftar artikel yang berbeda di layar:
Dengan cara ini kami dapat menerima array artikel untuk ketiga opsi - "topHeadLines", "pencarian "Dan" dari kategori ":
... tetapi untuk string pencarian tetap dan default searchString = "sports"
(jika diperlukan):
Namun, untuk opsi, "search"
Anda harus memberi pengguna bidang teks SearchView
untuk memasukkan string pencarian:
Akibatnya, pengguna dapat mencari berita apa pun dengan string pencarian yang diketik:
Untuk opsi, "from category"
diperlukan untuk menyediakan pengguna kesempatan untuk memilih kategori dan kami mulai dengan kategori science
:
sebagai hasilnya, pengguna dapat mencari berita pada kategori yang dipilih berita - science
, health
, business
, technology
:
kita dapat melihat bagaimana yang sangat sederhana ObservableObject
model yang memiliki dua dikendalikan oleh pengguna @Published
fitur - indexEndpoint
dansearchString
- memungkinkan Anda memilih berbagai informasi dari situs web NewsAPI.org .Daftar sumber informasi
Mari kita lihat bagaimana SwiftUI
"penerbit" sumber informasi yang diterima di kelas NewsAPI akan berfungsi fetchSources (for country: String) -> AnyPublisher<[Source], Never>
.Kami akan mendapatkan daftar sumber informasi untuk berbagai negara:
... dan kemampuan untuk mencarinya berdasarkan nama:
... serta informasi terperinci tentang sumber yang dipilih: nama, kategori, negara, deskripsi singkat, dan tautan ke situs:
Jika Anda mengklik tautan, kami akan mengunjungi situs web ini sumber informasi.Agar semua ini berfungsi, Anda memerlukan ObservableObject
Model yang sangat sederhana yang hanya memiliki dua @Published
properti yang dikontrol pengguna - searchString
dan country
:
Dan lagi, kami menggunakan skema yang sama: saat menginisialisasi instance kelas SourcesViewModel
kelas di init
kami membuat "langganan" yang akan beroperasi di seluruh "siklus hidup" instance kelas SourcesViewModel
dan memastikan bahwa daftar sumber informasi tergantung sources
pada negara country
dan string pencarian searchString
.Dengan bantuan Combine
kami menarik rantai dari "penerbit" $searchString
dan $country
untuk menghasilkan "penerbit" AnyPublisher<[Source], Never>
, yang nilainya adalah daftar sumber informasi. Kami "berlangganan" untuk itu menggunakan operator assign (to: \.sources, on: self)
, kami mendapatkan daftar sumber informasi yang kami butuhkan sources
. dan ingat AnyCancellable
"langganan" yang diterima dalam variabel cancellableSet
menggunakan operator .store ( in: &self.cancellableSet)
.Sekarang kita memiliki View Model
sumber informasi, mari mulai membuat UI
. B SwiftUI
untuk menyinkronkan View
cObservableObject
Model ini menggunakan @ObservedObject
variabel yang merujuk pada instance kelas Model ini.Tambahkan ContentViewSources
instance kelas ke struktur SourcesViewModel
dalam bentuk variabel var sourcesViewModel
, hapus Text ("Hello, World!")
dan tempatkan Anda sendiri View
untuk masing-masing 3 @Published
properti sourcesViewModel
: - kotak teks
SearchView
untuk bilah pencarian searchString
, -
Picker
untuk negara country
, - daftar
SourcesList
sumber informasi
Akibatnya, kami mendapatkan apa yang kami butuhkan View
:
Di layar ini, kami hanya mengelola string pencarian menggunakan kotak teks SearchView
dan "negara" dengan Picker
, dan sisanya terjadi secara OTOMATIS.Daftar sumber informasi SourcesList
berisi informasi minimal tentang setiap sumber - nama source.name
dan deskripsi singkat source.description
:
... tetapi memungkinkan Anda untuk mendapatkan informasi lebih rinci tentang sumber yang dipilih menggunakan tautan NavigationLink
di mana destination
kami menunjukkan DetailSourceView
sumber data mana yang merupakan sumber informasi source
dan contoh kelas yang diperlukan ArticlesViewModel
, yang memungkinkan dapatkan daftar artikelnya articles
:
Lihat betapa elegannya kita mendapatkan daftar artikel untuk sumber informasi yang dipilih di daftar sumber SourcesList
. Teman lama kita membantu kita - kelas ArticlesViewModel
yang harus kita atur @Published
properti "input" :- indeks
indexEndpoint = 3
, yaitu, opsi yang .articlesFromSource (_source:String)
sesuai dengan pemilihan artikel untuk sumber tetap source
, - string
searchString
sebagai sumber itu sendiri (atau lebih tepatnya pengenalnya) source.id
:
Secara umum, jika Anda melihat seluruh aplikasi NewsApp , Anda tidak akan melihat di mana pun bahwa kami secara eksplisit meminta pilihan artikel atau sumber informasi dari situs web NewsAPI.org . Kami hanya mengelola @Published
data, tetapi View Model
melakukan pekerjaan kami: memilih artikel dan sumber informasi yang kami butuhkan.Unduh gambar UIImage
untuk artikel Article
.
Model artikel Article
berisi URL
gambar urlToImage
yang menyertainya :
Berdasarkan ini, URL
di masa depan kita harus mendapatkan gambar sendiri UIImage
dari situs web NewsAPI.org .Kami sudah terbiasa dengan tugas ini. Di kelas ImageLoader
, menggunakan fungsi, fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>
buat "penerbit" AnyPublisher<UIImage?, Never>
dengan nilai gambar UIImage?
dan tidak ada kesalahan Never
(pada kenyataannya, jika kesalahan terjadi, maka gambar dikembalikan nil
). Anda dapat "berlangganan" ke "penerbit" ini untuk menerima gambar UIImage?
saat merancang antarmuka pengguna ( UI
). Sumber data untuk fungsi fetchImage(for url: URL?)
ini adalah url
, yang kami miliki:
Mari kita pertimbangkan secara rinci bagaimana formasi dengan bantuan Combine
"penerbit" ini terjadi AnyPublisher <UIImage?, Never>
, jika kita tahu url
:- jika
url
sama nil
, kembali Just(nil)
, - berdasarkan pada
url
bentuk "penerbit" dataTaskPublisher(for:)
, yang nilai outputnya Output
adalah tuple (data: Data, response: URLResponse)
dan kesalahan Failure
- URLError
, - kami hanya mengambil data
map {}
dari tuple (data: Data, response: URLResponse)
untuk diproses lebih lanjut data
, dan membentuk UIImage
, - jika langkah sebelumnya kesalahan kembali terjadi
nil
, - kami mengirimkan hasilnya ke
main
aliran, karena kami menganggap penggunaan lebih lanjut dalam desain UI
, - "Hapus" JENIS "penerbit" dan kembalikan salinannya
AnyPublisher
.
Anda melihat bahwa kodenya cukup kompak dan mudah dibaca, tidak ada callbacks
.Mari mulai membuat View Model
untuk gambar UIImage?
. Ini adalah kelas ImageLoader
yang mengimplementasikan protokol ObservableObject
, dengan dua @Published
properti:@Published url: URL?
adalah URL
gambar@Published var image: UIImage?
adalah gambar itu sendiri dari NewsAPI.org :
Dan lagi, ketika menginisialisasi instance kelas, ImageLoader
kita harus meregangkan rantai dari input "penerbit" $url
ke output "penerbit" AnyPublisher<UIImage?, Never>
, yang akan kita "berlangganan" nanti dan mendapatkan gambar yang kita butuhkan image
:
Kami menggunakan operator flatMap
dan "pelanggan" yang sangat sederhana assign (to: \image, on: self)
untuk menetapkannya ke yang diterima dari "penerbit" "Nilai ke properti @Published image
:
Dan lagi dalam variabel " berlangganan " cancellableSet
disimpan AnyCancellable
menggunakan operator store(in: &self.cancellableSet)
.Logika "pengunduh gambar" ini adalah Anda mengunduh gambar dari sesuatu selain yang nil URL
asalkan tidak dimuat sebelumnya, yaituimage == nil
. Jika selama proses pengunduhan kesalahan terdeteksi, gambar akan tidak ada, yaitu, image
akan tetap sama nil
.Di SwiftUI
kami menunjukkan gambar dengan bantuan ArticleImage
yang menggunakan instance dari imageLoader
kelas untuk ini ImageLoader
. Jika gambar gambarnya tidak sama nil
, maka itu ditampilkan menggunakan Image (...)
, tetapi jika itu sama nil
, maka tergantung pada apa yang sama dengan url
, apakah tidak ada yang ditampilkan EmptyView()
, atau persegi panjang Rectangle
dengan teks T berputar ditampilkan ext("Loading...")
:
Logika ini berfungsi dengan baik untuk kasus ini ketika Anda tahu pasti bahwa untuk url
, selain nil
Anda mendapatkan gambar image
, seperti halnya dengan database film TMDb . Dengan NewsAPI.org, agregator berita berbeda. Artikel-artikel dari beberapa sumber informasi memberikan yang berbeda dari nil URL
gambar, tetapi akses ke sana tertutup, dan kami mendapatkan persegi panjang Rectangle
dengan teks berputar Text("Loading...")
yang tidak akan pernah diganti:
Dalam situasi ini, jika URL
gambar berbeda dari nil
, maka kesetaraan nil
gambar image
dapat berarti bahwa gambar sedang memuat , dan fakta bahwa kesalahan terjadi saat memuat dan kami tidak akan pernah mendapatkan gambar image
. Untuk membedakan antara dua situasi ini, kami menambahkan satu lagi ImageLoader
ke dua @Published
properti yang ada di kelas : @Published var noData = false
- ini adalah nilai Boolean yang dengannya kami akan menunjukkan tidak adanya data gambar karena kesalahan selama pemilihan:
Saat membuat "berlangganan", kami init
menangkap semua kesalahan Error
yang terjadi saat memuat gambar dan mengakumulasi kehadiran mereka di @Published
properti self.noData = true
. Jika unduhan berhasil, maka kami mendapatkan gambar image
. Kami membuat"Penerbit" AnyPublisher<UIImage?, Error>
berdasarkan url
fungsi fetchImageErr (for url: URL?)
:
Kami mulai membuat metode fetchImageErr
dengan menginisialisasi "penerbit" Future
, yang dapat digunakan untuk secara asinkron memperoleh nilai TYPE tunggal Result
menggunakan penutupan. Penutupan memiliki satu parameter - Promise
yang merupakan fungsi dari TYPE (Result<Output, Failure>) → Void
: Kami akan mengubah
hasilnya Future
menjadiAnyPublisher <UIImage?, Error>
dengan bantuan operator "Hapus TIPE" eraseToAnyPublisher()
.Selanjutnya, kami akan melakukan langkah-langkah berikut, dengan mempertimbangkan semua kesalahan yang mungkin terjadi (kami tidak akan mengidentifikasi kesalahan, penting bagi kami untuk mengetahui bahwa ada kesalahan):0. periksa url
untuk nil
dan noData
pada true
: jika demikian, kembalikan kesalahan, jika tidak, transfer url
lebih lanjut dengan rantai,1. membuat "penerbit" dataTaskPublisher(for:)
yang inputnya adalah - url
, dan nilai output Output
adalah tuple (data: Data, response: URLResponse)
dan kesalahan URLError
,2. menganalisis menggunakan tryMap { }
tuple yang dihasilkan (data: Data, response: URLResponse)
: jika response.statusCode
berada dalam kisaran 200...299
, maka untuk pemrosesan lebih lanjut kami hanya mengambil data data
. Kalau tidak, kami "membuang" kesalahan (apa pun yang terjadi),3. kami map { }
mengubah data data
menjadi UIImage
,4. mengirimkan hasilnya ke main
aliran, karena kami berasumsi bahwa kami akan menggunakannya nanti dalam proses desain UI
- kami "berlangganan" ke "penerbit" yang diterima menggunakan sink
penutupan receiveCompletion
dan receiveValue
,- 5. jika kami receiveCompletion
menemukan kesalahan dalam penutupan error
, kami melaporkan menggunakannya promise (.failure(error)))
,- 6. pada penutupan, receiveValue
kami menginformasikan tentang penerimaan yang sukses dari array artikel yang menggunakan promise (.success($0))
,7. kita ingat "langganan" yang diterima dalam variabel cancellableSet
untuk memastikan kelangsungannya dalam "seumur hidup" dari instance kelas ImageLoader
,8. kita "menghapus" JENIS "penerbit" dan kembalikan instance AnyPublisher
.Kami kembali ke ArticleImage
tempat kami akan menggunakan @Published
variabel baru noData
. Jika tidak ada data gambar, maka kami tidak akan menampilkan apa pun, yaitu EmptyView ()
:
Akhirnya, kami akan mengemas semua kemungkinan kami menampilkan data dari agregator berita NewsAPI.org di TabView
:
Tampilkan kesalahan saat mengambil dan mendekode data JSON dari server NewsAPI.org .
Saat mengakses server NewsAPI.org , kesalahan dapat terjadi, misalnya, karena Anda menetapkan kunci yang salah API-key
atau, memiliki tarif pengembang yang tidak ada biaya, melebihi jumlah permintaan yang diizinkan atau hal lain. Pada saat yang sama, server NewsAPI.org memberi Anda HTTP
kode dan pesan yang sesuai:
Anda perlu menangani kesalahan server semacam ini. Jika tidak, pengguna aplikasi Anda akan jatuh ke dalam situasi ketika tiba-tiba, tanpa alasan, server NewsAPI.org akan berhenti memproses permintaan apa pun, membuat pengguna benar-benar bingung dengan layar kosong.Hingga saat ini, ketika memilih artikel [Article]
dan sumber informasi [Source]
dari server NewsAPI.org kami mengabaikan semua kesalahan, dan dalam kasus kemunculannya kembali array yang kosong [Article]()
dan sebagai hasilnya [Source]()
.Memulai dengan penanganan kesalahan, mari kita fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
buat NewsAPI
metode lain di kelas berdasarkan metode pemilihan artikel yang ada fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>
yang akan mengembalikan tidak hanya array artikel [Article]
, tetapi juga kemungkinan kesalahan NewsError
:func fetchArticlesErr(from endpoint: Endpoint) ->
AnyPublisher<[Article], NewsError> {
. . . . . . . .
}
Metode ini, serta metode ini fetchArticles
, menerima endpoint
dan mengembalikan "penerbit" pada input dengan nilai dalam bentuk array artikel [Article]
, tetapi alih-alih tidak ada kesalahan Never
, kami mungkin memiliki kesalahan yang ditentukan oleh pencacahan NewsError
:
Mari kita mulai membuat metode baru dengan menginisialisasi "penerbit" Future
, yang dapat berupa gunakan untuk secara sinkron mendapatkan nilai TYPE tunggal Result
menggunakan penutupan. Penutupan memiliki satu parameter - Promise
yang merupakan fungsi dari TYPE (Result<Output, Failure>) -> Void
: Kami akan mengubah yang
diterima Future
menjadi "penerbit" yang kami butuhkan AnyPublisher <[Article], NewsError>
menggunakan operator "TYPE Erase" eraseToAnyPublisher()
.Lebih lanjut dalam metode baru, fetchArticlesErr
kami akan mengulangi semua langkah yang kami ambil dalam metode fetchArticles
, tetapi kami akan memperhitungkan semua kemungkinan kesalahan:
- 0. endpoint
URL endpoint.absoluteURL
, url
nil
: nil
, .urlError
, — url
, - 1. «»
dataTaskPublisher(for:)
, — url
, Output
(data: Data, response: URLResponse)
URLError
, - 2.
tryMap { }
(data: Data, response: URLResponse)
: response.statusCode
200...299
, data
. «» .responseError
, data
, String
, , - 3.
JSON
, NewsResponse
, - 4.
main
, UI
- «» «»
sink
receiveCompletion
receiveValue
,
- 5.
receiveCompletion
error
, promise (.failure(...)))
, - 6.
receiveValue
promise (.success($0.articles))
,
- 7. «»
var subscriptions
, « » NewsAPI
, - 8. «» «»
AnyPublisher
.
Perlu dicatat bahwa "penerbit" dataTaskPublisher(for:)
berbeda dari prototipe dataTask
dalam hal dalam hal kesalahan server ketika response.statusCode
BUKAN dalam kisaran 200...299
, itu masih memberikan nilai keberhasilan dalam bentuk tuple (data: Data, response: URLResponse)
, dan bukan kesalahan dalam bentuk (Error, URLResponse?)
. Dalam hal ini, informasi galat server yang sebenarnya ada di data
. "Penerbit" dataTaskPublisher(for:)
memberikan kesalahan URLError
jika kesalahan terjadi di sisi klien (ketidakmampuan untuk menghubungi server, larangan sistem keamanan ATS
, dll.).Jika kita ingin menampilkan kesalahan SwiftUI
, maka kita perlu yang sesuai View Model
, yang akan kita panggil ArticlesViewModelErr
:
Di kelas ArticlesViewModelErr
yang mengimplementasikan protokol ObservableObject
, kali ini kita memiliki @Published
properti EMPAT :@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , Endpoint
: «» , ( «», View
), -
@Published var articles: [Article]
- ( «», NewsAPI.org ) -
@Published var articlesError: NewsError?
- , NewsAPI.org .
Ketika Anda menginisialisasi instance kelas ArticlesViewModelErr
, kita harus kembali memperpanjang rantai dari input "penerbit" $indexEndpoint
dan $searchString
untuk menghasilkan "penerbit" AnyPublisher<[Article],NewsError>
, yang kami "tandatangani" dengan "Pelanggan" sink
dan kami mendapatkan banyak artikel articles
atau kesalahan articlesError
.Di kelas kami, NewsAPI
kami telah membangun fungsi fetchArticlesErr (from endpoint: Endpoint)
yang mengembalikan "penerbit" AnyPublisher<[Article], NewsError>
, tergantung pada nilainya endpoint
, dan kami hanya perlu entah bagaimana menggunakan nilai-nilai "penerbit" $indexEndpoint
dan $searchString
mengubahnya menjadi argumen untuk fungsi ini endpoint
. Untuk memulainya, kami akan menggabungkan "penerbit" $indexEndpoint
dan $searchString
. Untuk melakukan ini, Combine
operator ada Publishers.CombineLatest
:
Kemudian kita harus menetapkan tipe kesalahan TYPE "publisher" sama dengan yang diperlukan NewsError
:
Selanjutnya, kita ingin menggunakan fungsi fetchArticlesErr (from endpoint: Endpoint)
dari kelas kita NewsAPI
. Seperti biasa, kami akan melakukan ini dengan bantuan operator flatMap
yang menciptakan "penerbit" baru berdasarkan data yang diterima dari "penerbit" sebelumnya:
Kemudian kami "berlangganan" ke "penerbit" yang baru diterima ini dengan bantuan "pelanggan" sink
dan menggunakan penutupannya receiveCompletion
dan receiveValue
untuk menerima dari "penerbit" baik nilai array artikel articles
atau kesalahan articlesError
:
Secara alami, perlu untuk mengingat "berlangganan" yang dihasilkan dalam beberapa init()
variabel eksternal cancellableSet
. Kalau tidak, kita tidak akan bisa mendapatkan nilainya secara sinkronarticles
atau kesalahan articlesError
setelah selesai init()
:
Untuk mengurangi jumlah panggilan ke server saat mengetik string pencarian searchString
, kita tidak boleh menggunakan "penerbit" dari bar pencarian itu sendiri $searchString
, tetapi versi yang dimodifikasi validString
:
"Berlangganan" ke "penerbit" ASYNCHRONOUS yang kami buat init( )
akan bertahan di seluruh "siklus hidup" instance kelas ArticlesViewModelErr
:
Kami melanjutkan ke koreksi kita UI
untuk menampilkan kemungkinan kesalahan pengambilan sampel data di atasnya. Dalam SwiftU
I, dalam struktur yang ada, kami ContentVieArticles
menggunakan yang lain, baru diperoleh View Model
, hanya menambahkan huruf "Err" dalam nama. Ini adalah turunan dari kelas. ArticlesViewModelErr
, yang “menangkap” kesalahan memilih dan / atau mendekode data artikel dari server NewsAPI.org :
Dan kami juga menambahkan tampilan pesan darurat Alert
jika terjadi kesalahan.Misalnya, jika kunci API yang salah adalah:struct APIConstants {
static let apiKey: String = "API_KEY"
. . . . . . . . . . . . .
}
... maka kami akan menerima pesan berikut:
Jika batas permintaan telah habis, kami akan menerima pesan berikut:
Kembali ke metode pemilihan artikel [Article]
dengan kemungkinan kesalahan NewsError
, kami dapat menyederhanakan kodenya jika kami menggunakan Generic
"penerbit" AnyPublisher<T,NewsError>,
yang, berdasarkan set, url
menerima JSON
informasi secara sinkron , menempatkannya secara langsung dalam Codable
Model T
dan melaporkan kesalahan NewsError
:
Seperti yang kita ketahui, kode ini sangat mudah digunakan untuk mendapatkan "penerbit" tertentu jika sumber datanya url
adalah agregator atau negara Endpoint
berita NewsAPI.orgcountry
sumber informasi, dan hasilnya memerlukan berbagai Model - misalnya, daftar artikel atau sumber informasi:

Kesimpulan
Kami belajar bagaimana mudahnya untuk memenuhi HTTP
permintaan dengan bantuan Combine
nya URLSession
“penerbit” dataTaskPublisher
dan Codable
. Jika Anda tidak perlu melacak kesalahan, Anda mendapatkan Generic
kode 5-baris yang sangat sederhana untuk "penerbit" AnyPublisher<T, Never>
, yang secara tidak sinkron menerima JSON
informasi dan menempatkannya langsung dalam Codable
Model T
berdasarkan yang diberikan url
:
Kode ini sangat mudah digunakan untuk mendapatkan penerbit tertentu, jika sumber datanya url
adalah, misalnya Endpoint
, dan output memerlukan berbagai Model - misalnya, satu set artikel atau daftar sumber informasi.Jika Anda perlu memperhitungkan kesalahan akun, maka kode untuk Generic
"penerbit" akan sedikit lebih rumit, tetapi tetap saja itu adalah kode yang sangat sederhana tanpa ada panggilan balik:
Menggunakan teknologi eksekusi HTTP
permintaan menggunakan Combine
, dapatkah Anda membuat "penerbit" AnyPublisher<UIImage?, Never>
yang secara sinkron memilih data dan menerima gambar UIImage? berdasarkan URL
. Pengunduh gambar di ImageLoade
-cache dalam memori untuk menghindari pengambilan data yang tidak sinkron berulang.Segala macam "penerbit" yang diperoleh dapat dengan mudah "dibuat bekerja" di kelas ObservableObject, yang menggunakan properti @Published mereka untuk mengontrol UI Anda yang dirancang menggunakan SwiftUI. Kelas-kelas ini biasanya memainkan peran Model Lihat, karena mereka memiliki apa yang disebut "input" @ properti yang diterbitkan yang sesuai dengan elemen UI aktif (TextField, Stepper, kotak teks Picker, tombol radio Toggle, dll.) Dan "output" @ properti yang diterbitkan , yang sebagian besar terdiri dari elemen UI pasif (teks teks, gambar gambar, lingkaran () bentuk geometris, persegi panjang (), dll.Gagasan ini menyebar ke seluruh aplikasi agregator berita NewsAPI.org yang disajikan dalam artikel ini. cukup universal dan digunakan saatmengembangkan aplikasi untuk basis data film TMDb dan agregator berita Hacker News , yang akan dibahas dalam artikel mendatang.Kode aplikasi untuk artikel ini ada di Github .PS1. Saya ingin menarik perhatian Anda pada fakta bahwa jika Anda menggunakan simulator untuk aplikasi yang disajikan dalam artikel ini, maka ketahuilah bahwa NavigationLink
simulator berfungsi dengan kesalahan. Kamu bisa menggunakanNavigationLink
pada simulator hanya 1 kali. Itu Anda menggunakan tautan, kembali, klik tautan yang sama - dan tidak ada yang terjadi. Sampai Anda menggunakan tautan lain, yang pertama tidak akan berfungsi, tetapi yang kedua tidak akan bisa diakses. Tapi ini hanya diamati pada simulator, pada perangkat nyata semuanya berfungsi dengan baik.2. Beberapa sumber informasi masih menggunakan http
sebagai gantinya https
"gambar" dari artikel mereka. Jika Anda benar-benar ingin melihat "gambar" ini, tetapi tidak dapat mengontrol sumber penampilannya, maka Anda harus mengonfigurasi sistem keamanan ATS ( App Transport Security)
untuk menerima http
"gambar" ini, tetapi ini, tentu saja, bukan ide yang baik . Anda dapat menggunakan opsi yang lebih aman .Referensi:
HTTP Swift 5 Combine SwiftUI. 1 .Modern Networking in Swift 5 with URLSession, Combine and Codable.URLSession.DataTaskPublisher’s failure typeCombine: Asynchronous Programming with Swift«SwiftUI & Combine: »Introducing Combine — WWDC 2019 — Videos — Apple Developer. session 722( 722 « Combine» )Combine in Practice — WWDC 2019 — Videos — Apple Developer. session 721( 721 « Combine» )