كود حديث لتقديم طلبات HTTP في Swift 5 باستخدام Combine واستخدامها في SwiftUI. الجزء الأول



HTTPيعد الاستعلام أحد أهم المهارات التي تحتاج إلى الحصول عليها عند تطوير iOSالتطبيقات. في الإصدارات السابقة Swift(قبل الإصدار 5) ، بغض النظر عما إذا كنت قد أنشأت هذه الطلبات من الصفر أو باستخدام إطار عمل Alamofire المعروف ، فقد انتهى بك الأمر إلى رمز معقد ومربك من callback النوع  completionHandler: @escaping(Result<T, APIError>) -> Void.

إن المظهر في Swift 5الإطار الجديد للبرمجة التفاعلية الوظيفية Combineبالاقتران مع الموجودة URLSession، Codableويزودك بكل الأدوات اللازمة لكتابة مستقلة لرمز مضغوط للغاية لجلب البيانات من الإنترنت.

في هذه المقالة ، وفقًا للمفهوم ، Combineسننشئ "ناشرين"Publisherلتحديد البيانات من شبكة الإنترنت، والذي يمكننا بسهولة "الاشتراك في" في المستقبل واستخدامها عند تصميم UIعلى حد سواء مع  UIKitوبمساعدة  SwiftUI.

كما  SwiftUIيبدو أكثر إيجازا وأكثر فعالية، وذلك لأن عمل "الناشرين"  Publisherلا يقتصر على بيانات عينة فقط، ويمتد مزيد حتى التحكم بالسطح البيني ( UI). والحقيقة هي أن SwiftUIفصل البيانات يتم  View باستخدام ObservableObjectفئات ذات @Publishedخصائص ، يتم SwiftUIمراقبة التغييرات  تلقائيًا وإعادة رسمها بالكامل View.

في هذه  ObservableObjectالفئات ، يمكنك ببساطة وضع منطق عمل معين للتطبيق ، إذا كان بعضًا من هذا@Published الخصائص هي نتيجة أخرى تحول متزامن و / أو غير متزامن @Published  الخصائص التي يمكن تغييرها مباشرة هذه العناصر "النشطة" واجهة المستخدم ( UI) مربعات النص TextField، Picker، Stepper، Toggleالخ

لتوضيح ما هو على المحك ، سأعطي أمثلة محددة. تقدم الآن العديد من الخدمات مثل  NewsAPI.org  و Hacker News مجمعي الأخبار لتقديم المستخدمين لاختيار مجموعات مختلفة من المقالات اعتمادًا على اهتماماتهم. في حالة مجمع الأخبار  NewsAPI.org ، يمكن أن يكون آخر الأخبار أو الأخبار في بعض الفئات - "الرياضة" أو "الصحة" أو "العلوم" أو "التكنولوجيا" أو "الأعمال" أو الأخبار من مصدر معلومات محدد "CNN" ، أخبار ABC ، ​​بلومبرج ، إلخ. عادة ما "يعبر" المستخدم عن رغباته في الخدمات بالشكل Endpointالذي يشكل المطلوب له URL.

لذا ، باستخدام الإطار  Combine، يمكنكObservableObject الفئات التي تستخدم رمزًا مضغوطًا للغاية (في معظم الحالات لا يزيد عن 10-12 سطرًا) مرة واحدة لتشكيل اعتماد متزامن و / أو غير متزامن لقائمة المقالات على  Endpointشكل "اشتراك" في @Published خصائص "سلبية" إلى خصائص "نشطة" @Published . سيكون هذا "الاشتراك" صالحًا طوال "دورة حياة" مثيل ObservableObject الفصل الدراسي بالكامل  . وبعد ذلك SwiftUI ستعطي المستخدم الفرصة لإدارة @Published الخصائص "النشطة" فقط في النموذج Endpoint، أي ما يريد رؤيته: ما إذا كانت ستكون مقالات تحتوي على آخر الأخبار أو المقالات في قسم "الصحة". سيتم توفير مظهر المقالات نفسها مع أحدث الأخبار أو المقالات في قسم "الصحة" على قسمك UI تلقائيًا بواسطة هذه  ObservableObject الفئات وخصائصها "السلبية" @ المنشورة. في الكودSwiftUI لن تحتاج أبدًا إلى طلب مجموعة مختارة من المقالات بشكل صريح ، لأن ObservableObjectالفصول التي تلعب دورًا هي المسؤولة عن عرضها الصحيح والمتزامن على الشاشة  View Model. سأوضح

لك كيفية عمل ذلك مع  NewsAPI.org  و Hacker News وقاعدة بيانات أفلام TMDb في سلسلة من المقالات. في جميع الحالات الثلاث ، سيعمل نمط الاستخدام نفسه تقريبًا  Combine، لأنه في التطبيقات من هذا النوع ، يتعين عليك دائمًا إنشاء قوائم من الأفلام أو المقالات ، واختيار "الصور" (الصور) التي تصاحبها ، والبحث في قواعد البيانات للأفلام أو المقالات المطلوبة باستخدام شريط البحث.

عند الوصول إلى هذه الخدمات ، قد تحدث أخطاء ، على سبيل المثال ، نظرًا لأنك حددت المفتاح الخطأ API-keyأو تجاوزت العدد المسموح به من الطلبات أو أي شيء آخر. تحتاج إلى معالجة هذا النوع من الأخطاء ، وإلا فإنك تخاطر بترك المستخدم تمامًا في حيرة مع شاشة فارغة. لذلك ، يجب أن تكون قادرًا ليس فقط على تحديد  Combine البيانات من الإنترنت باستخدام ، ولكن أيضًا الإبلاغ عن الأخطاء التي قد تحدث أثناء أخذ العينات والتحكم في مظهرها على الشاشة.

سنبدأ في تطوير استراتيجيتنا من خلال تطوير تطبيق يتفاعل مع مجمع الأخبار  NewsAPI.org . يجب أن أقول أنه في هذا التطبيق SwiftUIسيتم استخدامه إلى الحد الأدنى دون أي زخرفة ، فقط من أجل إظهار كيف Combineمع "الناشرين"Publisherو "الاشتراك" Subscriptionتتأثر UI.

يوصى بالتسجيل على موقع NewsAPI.org واستلام المفتاح APIالمطلوب لإكمال أي طلبات لخدمة NewsAPI.org . يجب وضعه في ملف NewsAPI.swift في البنية APIConstants.

رمز التطبيق لهذه المقالة على جيثب .

نموذج بيانات خدمة NewsAPI.org وواجهة برمجة التطبيقات


تتيح لك خدمة  NewsAPI.org تحديد معلومات حول المقالات الإخبارية الحالية [Article]ومصادرها  [Source]. سيكون نموذج البيانات الخاص بنا بسيطًا جدًا ، فهو موجود في ملف  Articles.swift :

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?
}

Articleستحتوي المقالة على المعرف idوالعنوان titleوالوصف  descriptionوالمؤلف authorوعنوان URL الخاص بالصورة urlToImageوتاريخ النشر publishedAtومصدر النشر source. فوق المقالات [Article]عبارة عن وظيفة إضافية NewsResponseسنكون مهتمين بها فقط في الممتلكات articles، وهي مجموعة من المقالات. بنية الجذر NewsResponseوهيكل  Articleو Codable، والتي سوف تسمح لنا حرفيا سطرين من التعليمات البرمجية فك رموز JSONالبيانات في نموذج. Article يجب أن يكون الهيكل  أيضًا Identifiable، إذا أردنا أن نسهل على أنفسنا عرض مجموعة من المقالات [Article]كقائمة  List في SwiftUI. Identifiable يتطلب البروتوكول وجود خاصيةidالتي سنزودها بمعرف اصطناعي فريد UUID(). سيحتوي

مصدر المعلومات  Sourceعلى معرف idواسم nameووصف  descriptionوبلد countryوفئة مصدر نشر categoryوعنوان URL للموقع url. فوق مصادر المعلومات ،  [Source] توجد وظيفة إضافية  SourcesResponseلن نهتم بها سوى خاصية sources، وهي مجموعة من مصادر المعلومات. بنية الجذر SourcesResponseوهيكل  Sourceو Codable، والتي سوف تسمح لنا بسهولة جدا لفك JSONالبيانات في نموذج. Source يجب أن يكون الهيكل  أيضًا Identifiable، إذا أردنا تسهيل عرض مجموعة من مصادر المعلومات  [Source]في شكل قائمة  List فيSwiftUI. Identifiableيتطلب البروتوكول وجود الممتلكات idالتي لدينا بالفعل ، لذلك لن تكون هناك حاجة إلى جهد إضافي منا.

ضع في اعتبارك الآن ما نحتاجه  APIلخدمة  NewsAPI.org وضعه في ملف  NewsAPI.swift . الجزء المركزي لدينا API هو فئة NewsAPIتقدم طريقتين لاختيار البيانات من مجمع الأخبار   NewsAPI.org - المقالات  [Article]ومصادر المعلومات  [Source]:

  • fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>  - اختيار المقالات  [Article]على أساس المعلمة endpoint،
  • fetchSources (for country: String) -> AnyPublisher<[Source], Never>- مجموعة مختارة من مصادر المعلومات [Source]لبلد معين country.

لا تعيد هذه الأساليب فقط مجموعة من المقالات  [Article] أو مجموعة من مصادر المعلومات  [Source]، ولكن "الناشرين"  Publisher المطابقين للإطار الجديد Combine. لا  يُرجع كلا الناشرين أي خطأ - Neverوإذا استمر حدوث خطأ في أخذ العينات أو التشفير ، فسيتم إرجاع مجموعة فارغة من المقالات  [Article]()أو مصادر المعلومات  [Source]()بدون أي رسالة توضح سبب إفراغ هذه المصفوفات. 

المقالات أو مصادر المعلومات التي نريد تحديدها من خادم NewsAPI.org ، سنشير إلى استخدام التعدادenum 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"
        }
    }
}

:

  • آخر الأخبار  .topHeadLines،
  • أخبار فئة معينة (الرياضة ، الصحة ، العلوم ، الأعمال ، التكنولوجيا)  .articlesFromCategory(_ category: String)،
  • أخبار من مصدر محدد للمعلومات (CNN ، ABC News ، Fox News ، إلخ)  .articlesFromSource(_ source: String)،
  • أي أخبار  .search (searchFilter: String)تلبي شرطًا معينًا searchFilter،
  • مصادر المعلومات .sources (country:String)لبلد معين country.

لتسهيل تهيئة الخيار الذي نحتاجه ، سنضيف Endpointمُهيئًا init?إلى التعداد لقوائم متنوعة من المقالات ومصادر المعلومات اعتمادًا على الفهرس index والسطر text، والذي له معان مختلفة لخيارات التعداد المختلفة:

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
        }
    }

دعنا نعود إلى الفصل NewsAPI وننظر بمزيد من التفصيل في الطريقة الأولى  fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>، التي تحدد المقالات [Article]بناءً على المعلمة endpointولا تعرض أي خطأ - 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
    }

  • على أساس endpoint نموذج URLالطلب ، قائمة المقالات المرغوبة endpoint.absoluteURL، إذا تعذر ذلك ، قم بإرجاع مجموعة فارغة من المقالات[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.

يتم تعيين مهمة اختيار مصادر المعلومات إلى الطريقة الثانية - fetchSources (for country: String) -> AnyPublisher<[Source], Never>وهي نسخة دلالية دقيقة للطريقة الأولى ، باستثناء أنه في هذه المرة بدلاً من المقالات [Article]، سنختار مصادر المعلومات [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
    }

ترجع لنا "الناشر" AnyPublisher <[Source], Never>بقيمة في شكل مصفوفة من مصادر المعلومات [Source] وغياب خطأ  Never (في حالة حدوث أخطاء ، يتم إرجاع مجموعة فارغة من المصادر  [ ]).

سنفرد الجزء المشترك من هاتين الطريقتين ، ونرتبها Genericكدالة fetch(_ url: URL) -> AnyPublisher<T, Error>تُرجع  Generic"الناشر" AnyPublisher<T, Error>بناءً على 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
    }

سيؤدي ذلك إلى تبسيط الطريقتين السابقتين:

//   
     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
    }

وبالتالي فإن "الناشرين" الذين تم الحصول عليهم لا يسلمون أي شيء حتى "يشترك" معهم. يمكننا القيام بذلك عند التصميم UI.

"الناشرون" Publisherكما View Model في SwiftUI. قائمة المقالات.


الآن القليل عن منطق الأداء SwiftUI.

إن SwiftUI التجريد الوحيد لـ "التغييرات الخارجية" التي يردون عليها Viewهو "الناشرين" Publisher. يمكن فهم "التغييرات الخارجية" كمؤقت Timer، NotificationCenter أو إخطار مع أو كائن النموذج الخاص بك ، والذي يمكن استخدامه باستخدام البروتوكول ObservableObjectإلى "مصدر الحقيقة" خارجي واحد (مصدر الحقيقة). 

بالنسبة إلى "الناشرين" العاديين اكتب Timerأو NotificationCenter Viewيتفاعل باستخدام الطريقة onReceive (_: perform:). مثال على استخدام "الناشر" Timerسنقدم لاحقًا في المقالة الثالثة حول إنشاء تطبيق لـ Hacker News .

في هذه المقالة ، سنركز على كيفية صنع نموذجنا SwiftUIخارجي "مصدر الحقيقة" (مصدر الحقيقة).

دعنا أولاً نلقي نظرة على كيفية عمل SwiftUI"الناشرين" المستلمين في مثال محدد لعرض أنواع مختلفة من المقالات:

.topHeadLines- آخر الأخبار ،  .articlesFromCategory(_ category: String) - أخبار فئة معينة ،  .articlesFromSource(_ source: String) - أخبار لمصدر محدد من المعلومات ، .search (searchFilter: String) - أخبار مختارة حسب حالة معينة.



بناءً على اختيار Endpointالمستخدم ، نحتاج إلى تحديث قائمة المقالات articlesالمحددة من NewsAPI.org . للقيام بذلك ، سننشئ فئة بسيطة جدًا  ArticlesViewModelتنفذ بروتوكولًا ObservableObject مع ثلاث  @Publishedخصائص:
 


  • @Published var indexEndpoint: IntEndpoint ( «», View),  
  • @Published var searchString: String — ,   ( «», View  TextField),
  • @Published var articles: [Article] - ( «», NewsAPI.org, «»).

في أقرب وقت وضعناها  @Published خصائص indexEndpoint أو searchString، يمكننا أن نبدأ في استخدامها على حد سواء كما الخصائص البسيطة  indexEndpoint و  searchString، وبأنها "الناشرين"  $indexEndpointو  $searchString.

في الفصل الدراسي  ArticlesViewModel، لا يمكنك فقط الإعلان عن الخصائص التي تهمنا ، ولكن أيضًا وصف منطق الأعمال لتفاعلها. تحقيقا لهذه الغاية ، عند تهيئة مثيل لفئة  ArticlesViewModel في ، initيمكننا إنشاء "اشتراك" يعمل طوال "دورة حياة" كامل لفئة الفصل  ArticlesViewModelوإعادة إنتاج اعتماد قائمة المقالات articlesعلى الفهرس  indexEndpoint وسلسلة البحث searchString.

للقيام بذلك، Combineونحن نعرب عن سلسلة "الناشرين" $indexEndpoint و $searchStringإخراج "الناشر"AnyPublisher<[Article], Never>قيمته قائمة بالمقالات  articles. ثم نقوم "بالاشتراك" فيه باستخدام عامل التشغيل assign (to: \.articles, on: self)ونحصل على قائمة بالمقالات التي نحتاجها  كخاصية articles "إخراج"  @Publishedتحدد UI.

يجب علينا سحب سلسلة NOT ببساطة من خصائص  indexEndpointو searchString، وهما من "الناشرين" $indexEndpointو $searchStringالذين يشاركون في خلق UIبمساعدة SwiftUIوسوف نقوم بتغيير لهم هناك باستخدام عناصر واجهة المستخدم  Pickerو TextField.

كيف سنفعل ذلك؟

لدينا بالفعل وظيفة في ترسانتنا fetchArticles (from: Endpoint)موجودة في الفصل  NewsAPIوترجع "ناشر" AnyPublisher<[Article], Never>، اعتمادًا على القيمةEndpointويمكننا استخدام فقط بطريقة أو بأخرى قيم "الناشرين"  $indexEndpointو  $searchStringتحويلها إلى حجة ل endpointهذه الوظيفة. 

اولا اجمع بين "الناشرين"  $indexEndpoint و  $searchString. لهذا ، Combineعامل التشغيل موجود Publishers.CombineLatest :



لإنشاء "ناشر" جديد بناءً على البيانات التي تم تلقيها من "الناشر" السابق Combine ، يتم استخدام عامل التشغيل  flatMap:



بعد ذلك ، "نشترك" في هذا "الناشر" المستلم حديثًا باستخدام "مشترك" بسيط للغاية  assign (to: \.articles, on: self)ونعين المستلم من " قيمة الناشر "  @Published للمصفوفة  articles:



لقد أنشأنا للتو init( )" ناشر "ASYNCHRONOUS و" مشترك "فيه ، نتيجةAnyCancellable"الاشتراك" وهذا من السهل التحقق منه إذا حافظنا على "اشتراكنا" ثابتًا let subscription:



الخاصية الرئيسية لـ AnyCancellable"الاشتراك" هي أنه بمجرد ترك نطاقه ، يتم تحرير الذاكرة التي يشغلها تلقائيًا. لذلك ، بمجرد init( ) اكتماله ، سيتم حذف هذا "الاشتراك" ARC، دون وجود وقت لتخصيص المعلومات غير المتزامنة التي يتم تلقيها مع تأخير زمني للصفيف articles. المعلومات غير المتزامنة ببساطة ليس لها مكان لـ "الأرض" ، بمعناها الحرفي ، "لقد ذهبت الأرض من تحت قدميها".

لحفظ مثل هذا "الاشتراك" ، من الضروري إنشاء init() متغير بعد المُهيئ var cancellableSetالذي سيحفظ AnyCancellable" اشتراكنا  " في هذا المتغير طوال "دورة حياة" مثيل الفصل بأكمله  ArticlesViewMode

لذلك ، نزيل الثابت let subscriptionونتذكر AnyCancellable" اشتراكنا " في المتغير  cancellableSetباستخدام عامل التشغيل .store ( in: &self.cancellableSet):



"الاشتراك" في "ناشر" ASYNCHRONOUS الذي أنشأناه init( )سيتم الاحتفاظ به طوال "دورة حياة" مثيل الفصل الدراسي بالكامل  ArticlesViewModel.

يمكننا تغيير معنى "الناشرين" $indexEndpointو / أو  بشكل تعسفي  searchString، ودائمًا بفضل "الاشتراك" الذي تم إنشاؤه ، سيكون لدينا مجموعة من المقالات التي تتوافق مع قيم هذين الناشرين  articlesدون أي جهد إضافي. ObservableObjectتسمى هذه  الفئة عادة  View Model.

لتقليل عدد المكالمات إلى الخادم عند كتابة سلسلة بحث searchString، يجب ألا نستخدم "ناشر" سلسلة البحث نفسها $searchString، ونسخته المعدلة validString:



الآن View Modelلدينا مقالاتنا ، لنبدأ في إنشاء واجهة المستخدم ( UI). في SwiftUIالتزامن Viewمع ObservableObject النموذج ، @ObservedObjectيتم استخدام متغير يشير إلى مثيل لفئة هذا النموذج. هذا الزوج -  ObservableObject الفئة  @ObservedObjectوالمتغير الذي يشير إلى مثيل هذه الفئة - هو الذي يتحكم في التغيير في واجهة المستخدم ( UI) في  SwiftUI.

نضيف إلى الهيكل ContentView مثيلًا للفئة ArticleViewModel في شكل متغير var articleViewModelونستبدلها Text ("Hello, World!")بقائمة من المقالات ArticlesListالتي نضع فيها المقالات التي  articlesViewModel.articlesتم الحصول عليها من موقعنا View Model. ونتيجة لذلك ، نحصل على قائمة بالمقالات لفهرس ثابت وافتراضي  indexEndpoint = 0، أي .topHeadLines لأحدث الأخبار:



أضف UIعنصرًا إلى شاشتنا  للتحكم في مجموعة المقالات التي نريد عرضها. سنستخدم  Pickerتغيير الفهرس $articlesViewModel.indexEndpoint. إن وجود الرمز  $إلزامي ، لأن هذا يعني تغييرًا في القيمة التي يوفرها  @Published "الناشر". يتم تشغيل "الاشتراك" في هذا "الناشر" على الفور ، والذي بدأناه في init ()،  سيتغير "الإخراج"  @Published"الناشر"  articlesوسنرى قائمة مختلفة من المقالات على الشاشة:



بهذه الطريقة يمكننا تلقي مجموعة من المقالات لجميع الخيارات الثلاثة - "topHeadLines" ، "بحث "و" من الفئة ":



... ولكن بالنسبة لسلسلة بحث ثابتة وافتراضية searchString = "sports"(حيث تكون مطلوبة):



ومع ذلك ، بالنسبة للخيار ،  "search" يجب عليك تزويد المستخدم بحقل نصي SearchViewلإدخال سلسلة البحث:



ونتيجة لذلك ، يمكن للمستخدم البحث عن أي أخبار عن طريق سلسلة البحث المكتوبة:



بالنسبة للخيار ،  "from category" من الضروري توفير المستخدم الفرصة لاختيار فئة ونبدأ بالفئة science:



ونتيجة لذلك ، يمكن للمستخدم البحث عن أي أخبار عن فئة الأخبار المختارة - science، health،  business، technology:



يمكننا أن نرى كيف ObservableObject أن نموذجًا بسيطًا جدًا  يحتوي على ميزتين يتحكم فيه المستخدم @Published - indexEndpoint وsearchString- يسمح لك باختيار مجموعة واسعة من المعلومات من موقع  NewsAPI.org .

قائمة مصادر المعلومات


دعونا نلقي نظرة على كيفية عمل SwiftUI "ناشر" مصادر المعلومات المستلمة في فصل NewsAPI fetchSources (for country: String) -> AnyPublisher<[Source], Never>.

سنحصل على قائمة بمصادر المعلومات لمختلف البلدان:



... والقدرة على البحث عنها بالاسم:



... بالإضافة إلى معلومات تفصيلية حول المصدر المحدد: اسمه وفئته وبلده ووصف قصير ورابط إلى الموقع: 



إذا نقرت على الرابط ، فسننتقل إلى موقع الويب الخاص بهذا مصدر المعلومات.

لكي يعمل كل هذا ، تحتاج إلى ObservableObjectنموذج بسيط للغاية  يحتوي على @Publishedخاصيتين يتحكم فيه المستخدم فقط - searchString و  country:



ومرة أخرى ، نستخدم نفس المخطط: عند تهيئة مثيل لفئة من SourcesViewModel الفئات في initنقوم بإنشاء "اشتراك" يعمل طوال "دورة حياة" مثيل الفصل الدراسي بالكامل ونتأكد من  SourcesViewModelأن قائمة مصادر المعلومات تعتمد  sourcesعلى البلد  countryوسلسلة البحث  searchString.

بمساعدة  Combineنحن سحب سلسلة من "الناشرين" $searchString و $countryإخراج "الناشر" AnyPublisher<[Source], Never>، الذي القيمة هي قائمة من مصادر المعلومات. نقوم "بالاشتراك" فيه باستخدام عامل التشغيل assign (to: \.sources, on: self)، نحصل على قائمة مصادر المعلومات التي نحتاجها  sources. وتذكر AnyCancellable"الاشتراك" المستلم  في متغير  cancellableSetباستخدام عامل التشغيل .store ( in: &self.cancellableSet).

الآن View Modelلدينا مصادر معلوماتنا ، فلنبدأ في الإنشاء UI. B SwiftUIللمزامنة ViewجObservableObject يستخدم النموذج @ObservedObjectمتغيرًا يشير إلى مثيل لفئة هذا النموذج.

أضف ContentViewSources مثيل الفئة إلى البنية  SourcesViewModelفي شكل متغير var sourcesViewModel، Text ("Hello, World!") وقم بإزالة  مكانك Viewلكل خاصية من @Publishedالخصائص الثلاثة   sourcesViewModel :

 
  • مربع نص  SearchViewلشريط البحث  searchString،
  •  Picker للبلد country،
  • قائمة  SourcesList مصادر المعلومات



ونتيجة لذلك ، نحصل على ما نحتاج إليه View:



في هذه الشاشة ، ندير سلسلة البحث فقط باستخدام مربع النص SearchViewو "البلد" مع  Picker، والباقي يحدث تلقائيًا.

تحتوي قائمة مصادر المعلومات SourcesListعلى الحد الأدنى من المعلومات حول كل مصدر - الاسم source.name ووصف موجز source.description:



... ولكنها تسمح لك بالحصول على معلومات أكثر تفصيلاً حول المصدر المحدد باستخدام الرابط NavigationLinkالذي destinationنشير DetailSourceViewفيه إلى  بيانات المصدر التي هي مصدر المعلومات  sourceوالنسخة المطلوبة من الفصل ArticlesViewModel، مما يسمح احصل على قائمة بمقالاته articles:



انظر كيف نحصل على قائمة المقالات لمصدر المعلومات المحدد في قائمة المصادر  SourcesList. يساعدنا صديقنا القديم - فئة  ArticlesViewModelيجب أن نحدد لها @Publishedخصائص "الإدخال"  :

  • الفهرس  indexEndpoint = 3، أي خيار  .articlesFromSource (_source:String)مقابل اختيار المقالات لمصدر ثابت source،
  • سلسلة  searchString كمصدر نفسه (أو بالأحرى معرفه) source.id :



بشكل عام ، إذا نظرت إلى تطبيق NewsApp بأكمله ، فلن ترى في أي مكان نطلب فيه صراحةً مجموعة مختارة من المقالات أو مصادر المعلومات من موقع  NewsAPI.org . نحن ندير @Published البيانات فقط  ، لكننا View Model نقوم بعملنا: يختار المقالات ومصادر المعلومات التي نحتاجها.

تنزيل الصورة UIImageللمقالة Article.


يحتوي نموذج المقالة  Article على URLصورة مرفقة به  urlToImage:



بناءً على ذلك ،  URLيجب علينا في المستقبل الحصول على الصور بأنفسهم UIImage من موقع  NewsAPI.org .

نحن بالفعل على دراية بهذه المهمة. في الفصل ImageLoader، باستخدام الوظيفة ، قم fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>بإنشاء "ناشر"  AnyPublisher<UIImage?, Never>بقيمة الصورة  UIImage? ولا يوجد خطأ  Never(في الواقع ، إذا حدثت أخطاء ، فسيتم إرجاع الصورة nil). يمكنك "الاشتراك" في هذا "الناشر" لتلقي الصور  UIImage? عند تصميم واجهة المستخدم ( UI). البيانات المصدر للدالة fetchImage(for url: URL?)هي  urlالتي لدينا:



دعنا نفكر بالتفصيل في كيفية عمل التكوين بمساعدة Combine"الناشر" AnyPublisher <UIImage?, Never>، إذا علمنا url:

  1. إذا كانت urlمتساوية nil، ارجع Just(nil)،
  2. بناء على urlشكل "الناشر" dataTaskPublisher(for:)، الذي قيمة الانتاج Outputهو الصفوف (tuple) (data: Data, response: URLResponse)وخطأ  FailureURLError،
  3. نأخذ فقط البيانات map {}من tuple (data: Data, response: URLResponse)لمزيد من المعالجة  data، والشكل UIImage،
  4. إذا حدث خطأ عودة الخطوات السابقة nil،
  5. نقدم النتيجة إلى mainالتدفق ، حيث نفترض المزيد من الاستخدام في التصميم UI،
  6. "مسح" نوع "الناشر" وإرجاع النسخة AnyPublisher.

ترى أن الشفرة مضغوطة تمامًا وقابلة للقراءة بشكل جيد ، لا يوجد أي منها callbacks.

لنبدأ في إنشاء  View Model الصورة UIImage?. هذه فئة ImageLoaderتطبق البروتوكول ObservableObject، مع  @Publishedخاصيتين:

  • @Published url: URL? هي URLصور
  • @Published var image: UIImage? هي الصورة نفسها من NewsAPI.org :



ومرة أخرى ، عند تهيئة مثيل من الفصل ،  ImageLoader يجب أن نمتد السلسلة من إدخال "الناشر"  $url إلى إخراج "الناشر" AnyPublisher<UIImage?, Never>، الذي  "سنشترك فيه" لاحقًا ونحصل على الصورة التي نحتاجها image:



نستخدم عامل التشغيل  flatMapو "مشترك" بسيط جدًا  assign (to: \image, on: self)لتعيينه إلى المستلم من "الناشر" يتم تخزين "قيم الخاصية @Published image:



ومرة أخرى في المتغير  " الاشتراك باستخدام عامل التشغيل  . منطق "تنزيل الصور" هذا هو أنك تقوم بتنزيل صورة من شيء آخر بخلاف تلك التي لم يتم تحميلها مسبقًا ، أيcancellableSet AnyCancellablestore(in: &self.cancellableSet)

nil URLimage == nil. إذا تم اكتشاف أي خطأ أثناء عملية التنزيل ، فستكون الصورة غائبة ، أي أنها imageستظل متساوية nil.

في SwiftUIعرض الصورة بالمساعدة ArticleImageالتي يستخدمها مثيل imageLoader الفئة لهذا الغرض ImageLoader. إذا كانت صورته غير متساوية nil، فسيتم عرضها باستخدام Image (...)، ولكن إذا كانت متساوية nil، فاعتمادًا على ما يساوي url ، إما أنه لا يتم عرض أي شيء EmptyView()، أو يتم عرض مستطيل Rectangleبه نص دوار T ext("Loading..."):



هذا المنطق يعمل بشكل جيد للحالة عندما تعلم على وجه اليقين أنه  urlبخلاف  nilالحصول على صورة image، كما هو الحال مع قاعدة بيانات الأفلام TMDb . مع NewsAPI.org ،   يختلف مجمع الأخبار  . تعطي مقالات بعض مصادر المعلومات صورة مختلفة عن  nil URLالصورة ، ولكن الوصول إليها مغلق ، ونحصل على مستطيل Rectangleبنص دوار Text("Loading...")لن يتم استبداله أبدًا:



في هذه الحالة ، إذا كانت  URLالصورة مختلفة عن  nilذلك ، فإن المساواة في  nilالصورة  imageقد تعني أن الصورة يتم تحميلها ، وحقيقة حدوث خطأ أثناء التحميل ولن نحصل أبدًا على صورة image. للتمييز بين هاتين الحالتين ، نضيف واحدة أخرى ImageLoader إلى خاصيتين @Publishedموجودتين في الفئة 

 @Published var noData = false - هذه قيمة منطقية سنشير من خلالها إلى عدم وجود بيانات الصورة بسبب خطأ أثناء التحديد:



عند إنشاء "اشتراك" ، نكتشف initجميع الأخطاء Errorالتي تحدث عند تحميل الصورة وتجميع تواجدها في  @Publishedالموقع self.noData = true. إذا نجح التنزيل ، نحصل على الصورة image. نقوم بإنشاء

"الناشر"  AnyPublisher<UIImage?, Error> على أساس  url الوظيفة fetchImageErr (for url: URL?):



نبدأ في إنشاء طريقة من fetchImageErrخلال تهيئة "الناشر"  Future، والتي يمكن استخدامها للحصول على قيمة TYPE واحدة بشكل غير متزامن Resultباستخدام الإغلاق. يحتوي الإغلاق على معلمة واحدة - Promiseوهي دالة من النوع  (Result<Output, Failure>) → Void: سنحول



النتيجة الناتجة FutureإلىAnyPublisher <UIImage?, Error>بمساعدة عامل التشغيل "Erase TYPE" eraseToAnyPublisher().

بعد ذلك، سنقوم بتنفيذ الخطوات التالية، مع الأخذ بعين الاعتبار جميع الأخطاء المحتملة (ونحن لن تحديد الأخطاء، هو ببساطة المهم بالنسبة لنا أن نعرف أن هناك خطأ):

0. الاختيار urlل nil و  noDataعلى true: إذا كان الأمر كذلك، ثم العودة الخطأ، إن لم يكن، ونقل urlمزيد من السلسلة ،
1. قم بإنشاء "ناشر" dataTaskPublisher(for:)يكون مدخله - urlوقيمة المخرجات Outputهي مجموعة (data: Data, response: URLResponse)وخطأ  URLError،
2. قم بالتحليل باستخدام المجموعة tryMap { } الناتجة (data: Data, response: URLResponse): إذا كان response.statusCodeفي النطاق 200...299، ثم للمعالجة الإضافية ، نأخذ البيانات فقط  data. خلاف ذلك ، فإننا "نتخلص" من الخطأ (بغض النظر عن ما) ،
3. نقوم map { }بتحويل البيانات dataإلى UIImage،
4. تسليم النتيجة إلى mainالتدفق ، حيث نفترض أننا سنستخدمها لاحقًا في عملية التصميم UI
- "نشترك" في "الناشر" المستلم باستخدام sinkعمليات الإغلاق ، receiveCompletionو - 5. إذا وجدنا  خطأ في الإغلاق  ، فإننا نبلغ باستخدامه ، - 6. في الختام ،  نبلغ عن الاستلام الناجح لمجموعة من المقالات باستخدام ، 7. نتذكر "الاشتراك" المستلم في المتغير  لضمان قابليته للتطبيق خلال "عمر" مثيل الفصل الدراسي ، 8. نحن "محو" TYPE "الناشر" وإرجاع المثيل .receiveValue
receiveCompletionerrorpromise (.failure(error)))
receiveValue promise (.success($0))
cancellableSetImageLoader
AnyPublisher

نعود إلى ArticleImageحيث سنستخدم @Publishedالمتغير الجديد  noData. إذا لم تكن هناك بيانات صورة ، فلن نعرض أي شيء ، أي EmptyView ():



في النهاية ، سنقوم بتعبئة جميع إمكانياتنا لعرض البيانات من مجمع أخبار NewsAPI.org في TabView:



عرض الأخطاء عند جلب بيانات JSON وفك تشفيرها من خادم  NewsAPI.org .


عند الوصول إلى خادم NewsAPI.org ،  يمكن أن تحدث أخطاء ، على سبيل المثال ، نظرًا لأنك حددت المفتاح الخطأ API-keyأو إذا كان لديك تعريفة مطور لا تكلف شيئًا ، تجاوز العدد المسموح به من الطلبات أو أي شيء آخر. في الوقت نفسه ،  يوفر لك خادم  NewsAPI.orgHTTP الرمز والرسالة المقابلة: من



الضروري معالجة هذا النوع من أخطاء الخادم. خلاف ذلك ، سيقع مستخدم تطبيقك في موقف عندما يتوقف خادم NewsAPI.org فجأة ، دون سبب ،   عن معالجة أي طلبات ، مما يترك المستخدم في حالة فقدان تام مع شاشة فارغة.

حتى الآن ، عند اختيار المقالات  [Article]ومصادر المعلومات  [Source]من خادم  NewsAPI.org تجاهلنا كل الأخطاء، وفي حال ورودها عاد صفائف فارغة [Article]()و  نتيجة لذلك  [Source]().

لنبدأ في معالجة الأخطاء ، دعنا fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never> ننشئ NewsAPI طريقة أخرى في الفصل استنادًا إلى طريقة اختيار المقالة  الحالية fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>التي ستعيد ليس فقط مجموعة من المقالات [Article]، ولكن أيضًا خطأ محتمل  NewsError:

func fetchArticlesErr(from endpoint: Endpoint) ->
                            AnyPublisher<[Article], NewsError> {

. . . . . . . .
}

هذه الطريقة ، بالإضافة إلى الطريقة fetchArticles، تقبل endpoint وتعيد "الناشر" عند الإدخال بقيمة في شكل مجموعة من المقالات [Article]، ولكن بدلاً من عدم وجود خطأ Never، قد يكون لدينا خطأ محدد من خلال التعداد NewsError:



نبدأ في إنشاء طريقة جديدة من خلال تهيئة "الناشر"  Future، والتي يمكن أن تكون استخدامها للحصول بشكل غير متزامن على قيمة TYPE واحدة Resultباستخدام الإغلاق. يحتوي الإغلاق على معلمة واحدة - Promiseوهي دالة من النوع TYPE  (Result<Output, Failure>) -> Void: سنحول



المستلم Futureإلى "الناشر" الذي نحتاج إليه  AnyPublisher <[Article], NewsError>باستخدام عامل التشغيل "TYPE Erase" eraseToAnyPublisher().

بعد ذلك ، في الطريقة الجديدة ، fetchArticlesErrسنكرر جميع الخطوات التي اتخذناها في الطريقة fetchArticles، لكننا سنأخذ في الاعتبار جميع الأخطاء المحتملة:



  • 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.

وتجدر الإشارة إلى أن "الناشر"  dataTaskPublisher(for:) يختلف عن نموذجه الأولي dataTaskفي أنه في حالة وجود خطأ في الخادم عندما response.statusCode لا يكون في النطاق 200...299، فإنه لا يزال يقدم القيمة الناجحة في شكل مجموعة (data: Data, response: URLResponse)، وليس خطأ في النموذج (Error, URLResponse?). في هذه الحالة ، يتم تضمين معلومات خطأ الخادم الحقيقي في data. dataTaskPublisher(for:) يقدم "الناشر" خطأ  URLErrorإذا حدث خطأ من جانب العميل (عدم القدرة على الاتصال بالخادم ، حظر نظام الأمان ATS، إلخ).

إذا أردنا عرض الأخطاء فيه SwiftUI، فنحن بحاجة إلى الخطأ المقابل View Model، الذي سنطلق عليه  ArticlesViewModelErr:



في الفصل  ArticlesViewModelErrالذي ينفذ البروتوكول ObservableObject ، هذه المرة لدينا أربع  @Publishedخصائص:

  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 .

عند تهيئة مثيل من فئة ArticlesViewModelErr، ويجب علينا مرة أخرى تمديد سلسلة من المدخلات "الناشرين" $indexEndpointو $searchStringإخراج "الناشر"  AnyPublisher<[Article],NewsError>، الذي نحن "وقعت" مع "المشترك" sinkونحصل على الكثير من المواد articlesأو خطأ  articlesError.

في صفنا، و NewsAPIنحن قد شيدت بالفعل وظيفة  fetchArticlesErr (from endpoint: Endpoint)أن عائدات "الناشر"  AnyPublisher<[Article], NewsError>، تبعا للقيمة endpoint، ونحن بحاجة فقط لاستخدام بطريقة أو بأخرى قيم "الناشرين"  $indexEndpointو  $searchStringتحويلها إلى حجة لهذه المهمة endpoint

بادئ ذي بدء ، سوف نقوم بدمج "الناشرين"  $indexEndpoint و  $searchString. للقيام بذلك ، Combineهناك عامل Publishers.CombineLatest:



ثم يجب علينا تعيين نوع الخطأ TYPE "ناشر" يساوي المطلوب  NewsError:



بعد ذلك ، نريد استخدام الوظيفة  fetchArticlesErr (from endpoint: Endpoint) من صفنا NewsAPI. كالعادة ، سنفعل ذلك بمساعدة عامل  flatMapإنشاء "ناشر" جديد على أساس البيانات الواردة من "الناشر" السابق:



ثم "الاشتراك" في هذا "الناشر" المستلم حديثًا بمساعدة "مشترك" sinkواستخدام عمليات الإغلاق receiveCompletionو receiveValueلتلقي من "الناشر" إما قيمة مجموعة من المقالات  articlesأو خطأ articlesError:



بطبيعة الحال ، من الضروري تذكر "الاشتراك" الناتج في بعض init()المتغيرات الخارجية cancellableSet. وإلا ، فلن نتمكن من الحصول على القيمة بشكل غير متزامنarticlesأو خطأ articlesError بعد الانتهاء init():



لتقليل عدد المكالمات إلى الخادم عند كتابة سلسلة بحث searchString، يجب ألا نستخدم "ناشر" شريط البحث نفسه  $searchString، ولكن نسخته المعدلة validString:



"اشتراك" في "ناشر" ASYNCHRONOUS الذي أنشأناه init( )سيكون تستمر طوال "دورة حياة" مثيل الفصل بالكامل  ArticlesViewModelErr:



ننتقل إلى تصحيح منطقتنا من UIأجل عرض الأخطاء المحتملة لأخذ عينات البيانات عليه. في SwiftUI ، في الهيكل الحالي ،  ContentVieArticles  نستخدم آخر ، تم الحصول عليه  View Modelللتو ، فقط بإضافة الأحرف "Err" في الاسم. هذه نسخة من الفصل.  ArticlesViewModelErr، الذي "يمسك" خطأ تحديد و / أو فك تشفير بيانات المقالة من خادم  NewsAPI.org :



كما نضيف عرض رسالة الطوارئ  Alert في حالة حدوث خطأ.

على سبيل المثال ، إذا كان مفتاح واجهة برمجة التطبيقات الخطأ هو:

struct APIConstants {
    // News  API key url: https://newsapi.org
    static let apiKey: String = "API_KEY" 
    
   .  .  .  .  .  .  .  .  .  .  .  .  .
}

... ثم سنتلقى الرسالة التالية:



إذا تم استنفاد الحد الأقصى للطلبات ، فسوف نتلقى الرسالة التالية:



بالعودة إلى طريقة تحديد المقالات  [Article] مع وجود خطأ محتمل  NewsError، يمكننا تبسيط رمزها إذا استخدمنا  Generic "الناشر" AnyPublisher<T,NewsError>,الذي ، بناءً على المجموعة ،  urlيتلقى JSONالمعلومات بشكل غير متزامن ، ويضعها مباشرة في Codableنموذج T وتقارير خطأ  NewsError:



كما نعلم، هذا الرمز هو من السهل جدا استخدام للحصول على محددة "الناشر" إذا كانت البيانات المصدر ل urlهي NewsAPI.orgEndpoint أخبار  مجمع  بلد أو country مصدر المعلومات ، ويتطلب الإخراج نماذج مختلفة - على سبيل المثال ، قائمة بالمقالات أو مصادر المعلومات:





استنتاج


تعلمنا كم هو سهل لتلبية HTTPطلبات بمساعدة Combineبها URLSession"الناشر" dataTaskPublisherو Codable. إذا لم تكن بحاجة إلى تتبع الأخطاء ، فستحصل على رمز بسيط للغاية مكون من Generic5 أسطر لـ "الناشر" AnyPublisher<T, Never>، والذي يتلقى JSON المعلومات بشكل غير متزامن ويضعها مباشرةً في Codable النموذج T بناءً على ما  urlيلي: من



السهل جدًا استخدام هذا الرمز للحصول على ناشر معين ، إذا كانت بيانات المصدر url ، على سبيل المثال Endpoint، ويتطلب الإخراج نماذج مختلفة - على سبيل المثال ، مجموعة من المقالات أو قائمة مصادر المعلومات.

إذا كنت بحاجة إلى أخذ الأخطاء في الاعتبار ، Genericفسيكون رمز  "الناشر" أكثر تعقيدًا بعض الشيء ، ولكنه سيظل رمزًا بسيطًا للغاية بدون أي عمليات رد:



باستخدام تقنية تنفيذ HTTPالاستعلام باستخدام Combine، هل يمكنك إنشاء "ناشر" AnyPublisher<UIImage?, Never>يختار البيانات بشكل غير متزامن ويتلقى صورة UIImage؟ على أساس URL. يتم ImageLoadeتنزيل برامج تنزيل الصور مؤقتًا في الذاكرة لتجنب الاسترداد المتكرر للبيانات غير المتزامنة.

يمكن بسهولة بسهولة "عمل" جميع أنواع "الناشرين" التي تم الحصول عليها في فئات ObservableObject ، التي تستخدم خصائصها المنشورة @ للتحكم في واجهة المستخدم الخاصة بك المصممة باستخدام SwiftUI. تلعب هذه الفئات عادةً دور نموذج العرض ، نظرًا لأنها تحتوي على ما يسمى بخصائص "الإدخال"Published التي تتوافق مع عناصر واجهة المستخدم النشطة (TextField ، Stepper ، مربعات النص Picker ، تبديل أزرار الراديو ، إلخ) و "الإخراج" @ الخصائص المنشورة ، تتكون بشكل رئيسي من عناصر واجهة المستخدم السلبية (النصوص النصية ، صور الصور ، الأشكال الهندسية (الدائرة) ، المستطيل () ، وما إلى

ذلك . تتخلل هذه الفكرة تطبيق مجمّع الأخبار NewsAPI.org بأكمله المقدم في هذه المقالة. عالمي جدا واستخدم عندماتطوير تطبيق ل TMDb فيلم قاعدة بيانات و أخبار هاكر مجمع الأخبار  ، والتي سيتم مناقشتها في المواد المستقبل.

رمز التطبيق لهذه المقالة على جيثب .

PS

1. أريد أن ألفت انتباهك إلى حقيقة أنه إذا كنت تستخدم المحاكي للتطبيق المقدم في هذه المقالة ، فاعلم أن NavigationLinkالمحاكي يعمل مع وجود خطأ. يمكنك استخدامNavigationLinkعلى جهاز المحاكاة مرة واحدة فقط. أولئك. لقد استخدمت الرابط ، وعادت ، وانقر على نفس الرابط - ولن يحدث شيء. حتى تستخدم رابطًا آخر ، لن يعمل الأول ، ولكن لن يكون الثاني ممكنًا. ولكن هذا يلاحظ فقط على المحاكي ، على جهاز حقيقي كل شيء يعمل بشكل جيد.

2. بعض مصادر المعلومات لا تزال تستخدم httpبدلا من ذلك من httpsأجل "صور" مقالاتهم. إذا كنت تريد بالتأكيد رؤية هذه "الصور" ، ولكن لا يمكنك التحكم في مصدر ظهورها ، فيجب عليك تكوين نظام الأمان ATS ( App Transport Security)لتلقي هذه http"الصور" ، ولكن هذه بالطبع ليست فكرة جيدة . يمكنك استخدام خيارات أكثر أمانًا .

المراجع:


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