Kode modern untuk membuat permintaan HTTP di Swift 5 menggunakan Combine dan menggunakannya di SwiftUI. Bagian 1



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, Toggledll

Untuk 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 {              // 0
                    return Just([Article]()).eraseToAnyPublisher()
        }
           return
            URLSession.shared.dataTaskPublisher(for:url)        // 1
            .map{$0.data}                                       // 2
            .decode(type: NewsResponse.self,                    // 3
                    decoder: APIConstants .jsonDecoder)
            .map{$0.articles}                                   // 4
            .replaceError(with: [])                             // 5
            .receive(on: RunLoop.main)                          // 6
            .eraseToAnyPublisher()                              // 7
    }

  • 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 {      // 0
                       return Just([Source]()).eraseToAnyPublisher()
           }
              return
               URLSession.shared.dataTaskPublisher(for:url)      // 1
               .map{$0.data}                                     // 2
               .decode(type: SourcesResponse.self,               // 3
                       decoder: APIConstants .jsonDecoder)
               .map{$0.sources}                                  // 4
               .replaceError(with: [])                           // 5
               .receive(on: RunLoop.main)                        // 6
               .eraseToAnyPublisher()                            // 7
    }

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:

//     URL
     func fetch<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
                   URLSession.shared.dataTaskPublisher(for: url)             // 1
                    .map { $0.data}                                          // 2
                    .decode(type: T.self, decoder: APIConstants.jsonDecoder) // 3
                    .receive(on: RunLoop.main)                               // 4
                    .eraseToAnyPublisher()                                   // 5
    }

Ini akan menyederhanakan dua metode sebelumnya:

//   
     func fetchArticles(from endpoint: Endpoint)
                                     -> AnyPublisher<[Article], Never> {
         guard let url = endpoint.absoluteURL else {
                     return Just([Article]()).eraseToAnyPublisher() // 0
         }
         return fetch(url)                                          // 1
             .map { (response: NewsResponse) -> [Article] in        // 2
                             return response.articles }
                .replaceError(with: [Article]())                    // 3
                .eraseToAnyPublisher()                              // 4
     }
    
    //    
    func fetchSources(for country: String)
                                       -> AnyPublisher<[Source], Never> {
        guard let url = Endpoint.sources(country: country).absoluteURL
            else {
                    return Just([Source]()).eraseToAnyPublisher() // 0
        }
        return fetch(url)                                         // 1
            .map { (response: SourcesResponse) -> [Source] in     // 2
                            response.sources }
               .replaceError(with: [Source]())                    // 3
               .eraseToAnyPublisher()                             // 4
    }

"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: IntEndpoint ( «», 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, healthbusiness, 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:

  1. jika urlsama nil, kembali Just(nil),
  2. berdasarkan pada urlbentuk "penerbit" dataTaskPublisher(for:), yang nilai outputnya Outputadalah tuple (data: Data, response: URLResponse)dan kesalahan  FailureURLError,
  3. kami hanya mengambil data map {}dari tuple (data: Data, response: URLResponse)untuk diproses lebih lanjut  data, dan membentuk UIImage,
  4. jika langkah sebelumnya kesalahan kembali terjadi nil,
  5. kami mengirimkan hasilnya ke mainaliran, karena kami menganggap penggunaan lebih lanjut dalam desain UI,
  6. "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.orgkesalahan 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  :

  1. @Published var indexEndpoint: IntEndpoint ( «», View), 
  2. @Published var searchString: String — ,  Endpoint: «» , ( «», View), 
  3.  @Published var articles: [Article] - ( «»,  NewsAPI.org )
  4.   @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 {
    // News  API key url: https://newsapi.org
    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 .

PS

1. 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 type
Combine: 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» )

All Articles