پیاده سازی معماری MVVM در SwiftUI با کمک Combine

آفلاین
user-avatar
آرمان آبکار
17 مرداد 1400, خواندن در 6 دقیقه

در این مقاله قصد داریم نحوه پیاده سازی الگوی طراحی قدرتمند MVVM را در برنامه های SwiftUI و به کمک چارچوب Combine بیاموزیم. ترکیب این سه مفهوم، استاندارد معماری برنامه های iOS در سالهای آینده خواهد بود. بنابراین بسیار مهم است که برای طراحی برنامه های سوئیفت در آینده، زمینه و دانش خوبی در این موارد داشته باشیم.

امروز ، Combine و SwiftUI را برسی می کنیم - دو چارچوبی که اپل اخیراً (2019) معرفی کرده است. در حال حاضر، آموزش های موجود هر چارچوب را به صورت جداگانه معرفی می کنند و آموزش می دهند. در این مقاله ما سعی می کنیم آنها را کنار هم قرار دهیم تا ببینیم چگونه با یکدیگر کار می کنند. همچنین ما از الگوی طراحی MVVM استفاده می کنیم که برای این دو چارچوب کاملاً مناسب است. بیاید شروع کنیم.

چرا MVVM محبوب ترین الگوی طراحی در iOS است؟

اگر تا به حال از RxSwift و RxCocoa استفاده کرده اید، شباهت های آنها و این دو تکنولوژی جدید را خواهید دید. اما حتی با RxCocoa، ما هنوز به مکانی برای اتصال داده ها از ViewModel به View و بالعکس نیاز داریم - که همان View Controller ها در دنیای قدیمی UIKit بود. بنابراین ، MVVM ماهیت واقعی خود را نمی توانست منعکس کند (View - ViewModel - Model) و حتی در هنگام استفاده از RxSwift و RxCocoa. در این مقاله MVVM را نیز به صورت عمیق تری تجزیه و تحلیل می کنیم.

اما با ترکیب Combine و SwiftUI است که الگوی طراحی MVVM به معنی واقعی می درخشد. زیرا در SwiftUI، این چارچوب iOS در واقع اجزای مورد نیاز برای به روز رسانی را دوباره رندر میکند. به عبارت دیگر مراحل Binding در Views انجام می شود (View Controller در اینجا دیگر وظایفی ندارد). بیایید با مثال برسی کنیم که در عمل چگونه کار می کند.

ما قصد داریم یک برنامه کاربردی ساده ایجاد کنیم که لیست نوشیدنی ها را از API بارگیری می کند - که پس اتمام همه گیری COVID-19 می توانید از آن بنوشید. بنابراین ما باید 3 شی اصلی را به شرح زیر ایجاد کنیم:

  • Brewery: The Model
  • Breweries: ViewModel
  • Breweries: View

پیاده سازی Model و View در SwiftUI

دلیل اینکه ما هر دوی این اشیا را در قسمت اول قرار می دهیم این است که آنها بسیار ساده هستند.

API ما که قصد استفاده از آن را داریم https: // api.openbrewerydb.org/breweries است. JSON آن اینگونه است:

[
  {
    "id": 2,
    "name": "Avondale Brewing Co",
    "brewery_type": "micro",
    "street": "201 41st St S",
    "city": "Birmingham",
    "state": "Alabama",
    "postal_code": "35222-1932",
    "country": "United States",
    "longitude": "-86.774322",
    "latitude": "33.524521",
    "phone": "2057775456",
    "website_url": "http://www.avondalebrewing.com",
    "updated_at": "2018-08-23T23:19:57.825Z"
  },
  {...}
]

بنابراین با چنین پاسخی از API ، مدل کلاس Brewery خواهد بود و به سادگی اطلاعات مورد نیاز ما را در خود ذخیره می کند. برای سادگی کار در اینجا ما فقط سه پارامتر را برای نمایش تعریف می کنیم. به عنوان یک چالش می توانید پارامترهای بیشتری برای نمایش در رابط کاربری (UI) شخصی خود دریافت کنید.

struct Brewery {
    let name: String
    let street: String
    let city: String
}

لایه View مورد بعدی است. ما دو View خواهیم داشت:

  • اولین View ، View اصلی ما است - BreweriesView. این View کاملاً ساده است و فقط شامل یک لیست در navigation view می باشد. داده ها موقتاً یک آرایه خالی هستند که نوع (type) آن Brewery است.
  • یک نکته مهم در اینجا این است که لیست باید بتواند در مجموعه ای از Brewery ها قابل پیمایش باشد، به این منظور ما باید مدل Brewery را مطابق پروتکل Hashable سازگار کنیم.
struct BreweriesView: View {
    let breweries = [Brewery]()
    var body: some View {
        NavigationView {
            List(breweries, id: \.self) {
                BreweryView(brewery: $0)
            }.navigationBarTitle("Breweries")
        }
    }
}

هر آیتم در لیست ما یک view سفارشی است - BreweryView. به این منظور به Brewery وابسته است - و از آن برای رندر رابط کاربری (UI) استفاده می کند. اینجا جایی است که می توانید از خلاقیت خود استفاده کنید.

struct BreweryView: View {
    private let brewery: Brewery
    init(brewery: Brewery) {
        self.brewery = brewery
    }

    var body: some View {
        HStack {
            Image(uiImage: UIImage(named: "beer")!)
                .resizable()
                .scaledToFit()
                .frame(width: 80, height: 80)
            VStack(alignment: .leading, spacing: 15) {
                Text(brewery.name)
                    .font(.system(size: 18))
                    .foregroundColor(Color.blue)
                Text("\(brewery.city) - \(brewery.street)")
                    .font(.system(size: 14))
            }
        }
    }
}

پیاده سازی ViewModel با Combine

مسئولیت اصلی این ViewModel دریافت داده ها از سرور است. پس از آن، به نوع (type) مدل ما رمزگشایی (decode) می کند و سپس آنها را به View متصل می کند. خوب، کار زیادی است پس بیایید برسی کنیم که چگونه این کار را باید انجام داد:

class BreweriesViewModel: ObservableObject {
    private let url = "https://api.openbrewerydb.org/breweries"
    func fetchBreweries() {
       // To-do: implement here
    }
}

از iOS 13 به بعد، URLSession نیز از Publisher داخلی پشتیبانی می کند، که در صورت اتمام کار، داده ها را منتشر می کند یا در صورت خطا در اتمام، خاتمه می یابد. این DataTaskPublisher است.

DataTaskPublisher

همانطور که در اینجا مشاهده می کنید ، خروجی ما، Data و URLRepsponse خواهد بود. بنابراین در این مرحله ، ما فقط به Data اهمیت می دهیم. در دنیای واقعی، می توانید از URLResponse را برای اعتبارسنجی موارد بیشتری از سرور مانند statusCode استفاده کنید. حالا بیایید آن را انجام دهیم:

func fetchBreweries() {
    URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
        .map { $0.data }
}

پس از در اختیار داشتن Data ، می توانیم آن را به مجموعه ای از Breweries رمزگشایی کنیم. به یاد داشته باشید که مدل ما باید مطابق با Decodable باشد:

func fetchBreweries() {
    URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
      .map { $0.data }
      .decode(type: [Brewery].self, decoder: JSONDecoder())
}

اما اگر Publisher با خطا متوقف شود چه؟ یا اگر Data نامعتبر داشته باشیم چه؟

در چنین مواردی ، از replaceError استفاده می کنیم. که هرگونه خطای موجود در جریان را با داده ارائه شده توسط ما جایگزین می کند. در این برنامه ، ما می خواهیم برای سادگی فقط یک آرایه خالی را برگردانیم که برای یک مقاله آموزش کافی است:

func fetchBreweries() {
    URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
      .map { $0.data }
      .decode(type: [Brewery].self, decoder: JSONDecoder())
      .replaceError(with: [])
}

در مرحله بعد ، ما باید نوع برگشتی را به AnyPublisher پاک کنیم و سپس آن را به یک پراپرتی اختصاص دهیم که می تواند مقدار را برای View منتشر کند. از آنجا که Data باید در رابط کاربری (UI) رندر شود پس باید آن را در MainThread دریافت کنیم. بنابراین ، کلاس ViewModel نهایی به این شکل است:

class BreweriesViewModel: ObservableObject {
    private let url = "https://api.openbrewerydb.org/breweries"
    private var task: AnyCancellable?

    @Published var breweries: [Brewery] = []

    func fetchBreweries() {
        task = URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
            .map { $0.data }
            .decode(type: [Brewery].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .eraseToAnyPublisher()
            .receive(on: RunLoop.main)
            .assign(to: \BreweriesViewModel.breweries, on: self)
    }
}

معماری MVVM با Combine در SwiftUI

اکنون نوبت آن است که همه قطعات را به هم متصل کنیم، تا کاملاً درک کنیم که چگونه می توان معماری MVVM را با Combine در برنامه های SwiftUI به دست آورد. در BreweriesView، ما مستقیماً ViewModel را صدا می زنیم و لیستی که نیاز داریم هم از همان ViewModel می آید.

پس از تنظیم همه چیز، فقط کافی است تا fetchBreweries () را در onAppear فراخوانی کنیم.

struct BreweriesView: View {
    @ObservedObject var viewModel = BreweriesViewModel()
    var body: some View {
        NavigationView {
            List(viewModel.breweries, id: \.self) {
                BreweryView(brewery: $0)
            }.navigationBarTitle("Breweries")
                .onAppear {
                    self.viewModel.fetchBreweries()
            }
        }
    }
}

پس از اجرای برنامه iOS ما در شبیه ساز، با چنین چیزی مواجه می شویم:

اجرای برنامه

نتیجه

تبریک می گوییم ، ما در این مقاله یک برنامه iOS ایجاد کرده ایم که دارای معماری تمیز و مدرن MVVM است و از جدیدترین چارچوب های اپل - Combine و SwiftUI استفاده می کند.

اگرچه این برنامه بسیار ساده است، ولی تمام مفاهیم مربوط به SwiftUI ، Combine ، MVVM و مهمتر از همه نحوه عملکرد آنها با هم را نشان می دهد. از لحاظ تئوری، MVVM بیشتر برای چارچوب های برنامه نویسی واکنشی (reactive programming) مناسب است، به این ترتیب ما مسئولیت های ماژول ها را به وضوح تقسیم می کنیم (در این پروژه، لایه ارائه دهنده یا presentation و لایه رابط کاربری یا UI). همچنین همانطور که خودتان می توانید پیش بینی کنید، احتمالاً SwiftUI و Combine معماری قدیمی MVC را حذف خواهند کرد. پس تغییرات بزرگی در راه است و ما باید خود را با آن وفق دهیم، درست است؟

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
آرمان آبکار @armanabkar
توسعه دهند موبایل (iOS) و فرانت اند
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو