Permintaan HTTPadalah salah satu keterampilan paling penting yang perlu Anda dapatkan ketika mengembangkan iOSaplikasi. 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 5kerangka baru pemrograman reaktif fungsional Combinedalam hubungannya dengan yang ada URLSession, dan Codablememberi Anda semua alat yang diperlukan untuk menulis kode yang sangat kompak secara independen untuk mengambil data dari Internet.Dalam artikel ini, sesuai dengan konsepnya, Combinekami akan membuat "penerbit"Publisheruntuk memilih data dari Internet, yang dapat dengan mudah kita “berlangganan” di masa depan dan digunakan saat mendesain UIdengan UIKitdan dengan bantuan SwiftUI.Karena SwiftUIterlihat lebih ringkas dan lebih efektif, karena tindakan "penerbit" Publishertidak terbatas hanya pada data sampel, dan meluas lebih jauh hingga ke kontrol antarmuka pengguna ( UI). Faktanya adalah bahwa SwiftUIpemisahan data View dilakukan menggunakan ObservableObjectkelas dengan @Publishedproperti, perubahan yang secara otomatis SwiftUIdipantau dan sepenuhnya digambar ulang View.Di ObservableObjectkelas - 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, ToggledllUntuk 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 Endpointyang 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 Endpointdalam 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 ObservableObjectkelas 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-keyatau 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 SwiftUIakan digunakan seminimal mungkin tanpa embel-embel dan hanya untuk menunjukkan bagaimana Combinedengan "penerbit" nyaPublisherdan "berlangganan" Subscriptionterpengaruh UI.Disarankan agar Anda mendaftar di situs web NewsAPI.org dan menerima kunci APIyang 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 Articleakan berisi pengidentifikasi id, judul title, deskripsi description, penulis author, URL dari "gambar" urlToImage, tanggal publikasi publishedAtdan sumber publikasi source. Di atas artikel [Article]adalah add- NewsResponsein di mana kita hanya akan tertarik pada properti articles, yang merupakan array artikel. Struktur NewsResponsedan struktur root Articleadalah Codable, yang akan memungkinkan kita untuk secara harfiah dua baris kode decoding JSONdata 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 propertiidyang akan kami berikan dengan pengidentifikasi unik buatan UUID().Sumber informasi Sourceakan berisi pengidentifikasi id, nama name, deskripsi description, negara country, kategori sumber publikasi category, URL situs url. Di atas sumber informasi [Source] ada add- SourcesResponsein di mana kita hanya akan tertarik pada properti sources, yang merupakan array dari sumber informasi. Struktur SourcesResponsedan struktur root Sourceadalah Codable, yang akan memungkinkan kita untuk dengan mudah mendekode JSONdata menjadi model. Struktur Source juga harus Identifiable, jika kita ingin memfasilitasi tampilan berbagai sumber informasi [Source]dalam bentuk daftar List diSwiftUI. Protokol Identifiablemensyaratkan keberadaan properti idyang sudah kita miliki, jadi tidak ada upaya tambahan yang diperlukan dari kami.Sekarang pertimbangkan apa yang kita butuhkan APIuntuk layanan NewsAPI.org dan letakkan di file NewsAPI.swift . Bagian utama dari kami API adalah kelas NewsAPIyang 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 - Neverdan 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 Endpointinisialisasi 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 endpointdan 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 URLuntuk 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 Genericfungsi 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" Publisherseperti View Model pada SwiftUI. Daftar artikel.
Sekarang sedikit tentang logika fungsi SwiftUI.Satu- SwiftUI satunya abstraksi dari "perubahan eksternal" yang mereka respons Viewadalah "penerbit" Publisher. "Perubahan eksternal" dapat dipahami sebagai timer Timer, pemberitahuan dengan NotificationCenter atau objek Model Anda, yang menggunakan protokol ObservableObjectdapat diubah menjadi "sumber kebenaran" tunggal eksternal (sumber kebenaran). Untuk tipe "penerbit" biasa Timeratau NotificationCenter Viewbereaksi menggunakan metode onReceive (_: perform:). Contoh penggunaan "penerbit" Timerakan kami sajikan nanti di artikel ketiga tentang pembuatan aplikasi untuk Hacker News .Pada artikel ini, kita akan fokus pada bagaimana membuat Model kita SwiftUIeksternal "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 Endpointpengguna mana yang dipilih, kita perlu memperbarui daftar artikel yang articlesdipilih dari NewsAPI.org . Untuk melakukan ini, kita akan membuat kelas ArticlesViewModelyang sangat sederhana yang mengimplementasikan protokol ObservableObject dengan tiga @Publishedproperti: 
@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" $indexEndpointdan $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 , initkita dapat membuat "langganan" yang akan bertindak di seluruh "siklus hidup" instance kelas ArticlesViewModeldan mereproduksi ketergantungan dari daftar artikel articlespada indeks indexEndpoint dan string pencarian searchString.Untuk melakukan ini, Combinekami memperluas rantai "penerbit" $indexEndpoint dan $searchStringmenghasilkan "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 indexEndpointdan searchString, yaitu dari "penerbit" $indexEndpointdan $searchStringyang berpartisipasi dalam pembuatan UIdengan bantuan SwiftUIdan kami akan mengubahnya di sana menggunakan elemen antarmuka pengguna Pickerdan TextField.Bagaimana kita akan melakukan ini?Kami sudah memiliki fungsi di gudang senjata kami fetchArticles (from: Endpoint)yang ada di kelas NewsAPIdan mengembalikan "penerbit" AnyPublisher<[Article], Never>, tergantung pada nilainyaEndpoint, dan kami hanya bisa menggunakan nilai "penerbit" $indexEndpointdan $searchStringmengubahnya menjadi argumen untuk endpointfungsi ini. Pertama, gabungkan "penerbit" $indexEndpoint dan $searchString. Untuk melakukan ini, Combineoperator 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 cancellableSetyang akan menyimpan AnyCancellable"langganan" kami dalam variabel ini di seluruh "siklus hidup" instance kelas ArticlesViewMode. Oleh karena itu, kami menghapus konstanta let subscriptiondan mengingat AnyCancellable"langganan" kami dalam variabel cancellableSetmenggunakan 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" $indexEndpointdan / atau searchString, dan selalu berkat "berlangganan" yang dibuat, kami akan memiliki serangkaian artikel yang sesuai dengan nilai-nilai dari dua penerbit articlestanpa upaya tambahan. Ini ObservableObjectkelas 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 Modelartikel, mari kita mulai membuat antarmuka pengguna ( UI). Untuk SwiftUImenyinkronkan Viewdengan ObservableObject Model, @ObservedObjectvariabel digunakan yang merujuk pada instance kelas Model ini. Pasangan ini - ObservableObject kelas dan @ObservedObjectvariabel 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 articleViewModeldan menggantinya Text ("Hello, World!")dengan daftar artikel ArticlesListdi mana kami menempatkan artikel yang articlesViewModel.articlesdiperoleh dari kami View Model. Akibatnya, kami mendapatkan daftar artikel untuk indeks tetap dan default indexEndpoint = 0, yaitu, untuk .topHeadLines berita terbaru:
Tambahkan UIelemen ke layar kami untuk mengontrol set artikel mana yang ingin kami tampilkan. Kami akan menggunakan Pickerperubahan 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 SearchViewuntuk 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 ObservableObjectModel yang sangat sederhana yang hanya memiliki dua @Publishedproperti yang dikontrol pengguna - searchString dan country:
Dan lagi, kami menggunakan skema yang sama: saat menginisialisasi instance kelas SourcesViewModel kelas di initkami membuat "langganan" yang akan beroperasi di seluruh "siklus hidup" instance kelas SourcesViewModeldan memastikan bahwa daftar sumber informasi tergantung sourcespada negara countrydan string pencarian searchString.Dengan bantuan Combinekami menarik rantai dari "penerbit" $searchString dan $countryuntuk 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 cancellableSetmenggunakan operator .store ( in: &self.cancellableSet).Sekarang kita memiliki View Modelsumber informasi, mari mulai membuat UI. B SwiftUIuntuk menyinkronkan ViewcObservableObject Model ini menggunakan @ObservedObjectvariabel yang merujuk pada instance kelas Model ini.Tambahkan ContentViewSources instance kelas ke struktur SourcesViewModeldalam bentuk variabel var sourcesViewModel, hapus Text ("Hello, World!") dan tempatkan Anda sendiri Viewuntuk masing-masing 3 @Publishedproperti sourcesViewModel : - kotak teks
SearchViewuntuk 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 SearchViewdan "negara" dengan Picker, dan sisanya terjadi secara OTOMATIS.Daftar sumber informasi SourcesListberisi 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 NavigationLinkdi mana destinationkami menunjukkan DetailSourceViewsumber data mana yang merupakan sumber informasi sourcedan 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 ArticlesViewModelyang harus kita atur @Publishedproperti "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 UIImageuntuk artikel Article.
Model artikel Article berisi URLgambar urlToImageyang menyertainya :
Berdasarkan ini, URLdi 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
urlsama nil, kembali Just(nil), - berdasarkan pada
urlbentuk "penerbit" dataTaskPublisher(for:), yang nilai outputnya Outputadalah 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
mainaliran, 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 ImageLoaderyang mengimplementasikan protokol ObservableObject, dengan dua @Publishedproperti:@Published url: URL? adalah URLgambar@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 flatMapdan "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 AnyCancellablemenggunakan operator store(in: &self.cancellableSet).Logika "pengunduh gambar" ini adalah Anda mengunduh gambar dari sesuatu selain yang nil URLasalkan tidak dimuat sebelumnya, yaituimage == nil. Jika selama proses pengunduhan kesalahan terdeteksi, gambar akan tidak ada, yaitu, imageakan tetap sama nil.Di SwiftUIkami menunjukkan gambar dengan bantuan ArticleImageyang 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 Rectangledengan teks T berputar ditampilkan ext("Loading..."):
Logika ini berfungsi dengan baik untuk kasus ini ketika Anda tahu pasti bahwa untuk url, selain nilAnda 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 URLgambar, tetapi akses ke sana tertutup, dan kami mendapatkan persegi panjang Rectangledengan teks berputar Text("Loading...")yang tidak akan pernah diganti:
Dalam situasi ini, jika URLgambar berbeda dari nil, maka kesetaraan nilgambar imagedapat 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 @Publishedproperti 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 initmenangkap semua kesalahan Erroryang terjadi saat memuat gambar dan mengakumulasi kehadiran mereka di @Publishedproperti 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 fetchImageErrdengan menginisialisasi "penerbit" Future, yang dapat digunakan untuk secara asinkron memperoleh nilai TYPE tunggal Resultmenggunakan penutupan. Penutupan memiliki satu parameter - Promiseyang merupakan fungsi dari TYPE (Result<Output, Failure>) → Void: Kami akan mengubah
hasilnya FuturemenjadiAnyPublisher <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 urluntuk nil dan noDatapada true: jika demikian, kembalikan kesalahan, jika tidak, transfer urllebih lanjut dengan rantai,1. membuat "penerbit" dataTaskPublisher(for:)yang inputnya adalah - url, dan nilai output Outputadalah tuple (data: Data, response: URLResponse)dan kesalahan URLError,2. menganalisis menggunakan tryMap { } tuple yang dihasilkan (data: Data, response: URLResponse): jika response.statusCodeberada 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 datamenjadi UIImage,4. mengirimkan hasilnya ke mainaliran, karena kami berasumsi bahwa kami akan menggunakannya nanti dalam proses desain UI- kami "berlangganan" ke "penerbit" yang diterima menggunakan sinkpenutupan receiveCompletiondan 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 cancellableSetuntuk memastikan kelangsungannya dalam "seumur hidup" dari instance kelas ImageLoader,8. kita "menghapus" JENIS "penerbit" dan kembalikan instance AnyPublisher.Kami kembali ke ArticleImagetempat kami akan menggunakan @Publishedvariabel 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-keyatau, memiliki tarif pengembang yang tidak ada biaya, melebihi jumlah permintaan yang diizinkan atau hal lain. Pada saat yang sama, server NewsAPI.org memberi Anda HTTPkode 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 Resultmenggunakan penutupan. Penutupan memiliki satu parameter - Promiseyang merupakan fungsi dari TYPE (Result<Output, Failure>) -> Void: Kami akan mengubah yang
diterima Futuremenjadi "penerbit" yang kami butuhkan AnyPublisher <[Article], NewsError>menggunakan operator "TYPE Erase" eraseToAnyPublisher().Lebih lanjut dalam metode baru, fetchArticlesErrkami 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 dataTaskdalam 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 URLErrorjika 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 ArticlesViewModelErryang mengimplementasikan protokol ObservableObject , kali ini kita memiliki @Publishedproperti 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" $indexEndpointdan $searchStringuntuk menghasilkan "penerbit" AnyPublisher<[Article],NewsError>, yang kami "tandatangani" dengan "Pelanggan" sinkdan kami mendapatkan banyak artikel articlesatau kesalahan articlesError.Di kelas kami, NewsAPIkami 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" $indexEndpointdan $searchStringmengubahnya menjadi argumen untuk fungsi ini endpoint. Untuk memulainya, kami akan menggabungkan "penerbit" $indexEndpoint dan $searchString. Untuk melakukan ini, Combineoperator 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 flatMapyang menciptakan "penerbit" baru berdasarkan data yang diterima dari "penerbit" sebelumnya:
Kemudian kami "berlangganan" ke "penerbit" yang baru diterima ini dengan bantuan "pelanggan" sinkdan menggunakan penutupannya receiveCompletiondan receiveValueuntuk menerima dari "penerbit" baik nilai array artikel articlesatau 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 sinkronarticlesatau 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 UIuntuk menampilkan kemungkinan kesalahan pengambilan sampel data di atasnya. Dalam SwiftUI, 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, urlmenerima JSONinformasi secara sinkron , menempatkannya secara langsung dalam CodableModel T dan melaporkan kesalahan NewsError:
Seperti yang kita ketahui, kode ini sangat mudah digunakan untuk mendapatkan "penerbit" tertentu jika sumber datanya urladalah agregator atau negara Endpointberita NewsAPI.orgcountry sumber informasi, dan hasilnya memerlukan berbagai Model - misalnya, daftar artikel atau sumber informasi:

Kesimpulan
Kami belajar bagaimana mudahnya untuk memenuhi HTTPpermintaan dengan bantuan Combinenya URLSession“penerbit” dataTaskPublisherdan Codable. Jika Anda tidak perlu melacak kesalahan, Anda mendapatkan Generickode 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 HTTPpermintaan 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 NavigationLinksimulator berfungsi dengan kesalahan. Kamu bisa menggunakanNavigationLinkpada 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 httpsebagai 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» )