توسعه برنامه در SwiftUI. بخش 1: Dataflow و Redux

توسعه برنامه در SwiftUI. بخش 1: Dataflow و Redux

پس از شرکت در جلسه وضعیت اتحادیه در WWDC 2019، تصمیم گرفتم در SwiftUI عمیق شوم. من زمان زیادی را صرف کار با آن کرده ام و اکنون شروع به توسعه یک برنامه واقعی کرده ام که می تواند برای طیف گسترده ای از کاربران مفید باشد.

من آن را MovieSwiftUI نامیدم - این برنامه ای برای جستجوی فیلم های جدید و قدیمی و همچنین جمع آوری آنها در یک مجموعه با استفاده از TMDB API. من همیشه عاشق فیلم بوده‌ام و حتی مدت‌ها پیش شرکتی در این زمینه ایجاد کردم. این شرکت را به سختی می توان باحال نامید، اما برنامه این بود!

یادآوری می کنیم: برای همه خوانندگان "Habr" - تخفیف 10 روبل هنگام ثبت نام در هر دوره Skillbox با استفاده از کد تبلیغاتی "Habr".

Skillbox توصیه می کند: دوره آموزشی آنلاین "توسعه دهنده حرفه ای جاوا".

بنابراین MovieSwiftUI چه کاری می تواند انجام دهد؟

  • با API تعامل دارد - تقریباً هر برنامه مدرن این کار را انجام می دهد.
  • داده های ناهمزمان را روی درخواست ها بارگیری می کند و JSON را با استفاده از مدل سوئیفت تجزیه می کند قابل کدگذاری.
  • تصاویر بارگیری شده در صورت درخواست را نشان می دهد و آنها را در حافظه پنهان نگه می دارد.
  • این برنامه برای iOS، iPadOS و macOS بهترین UX را برای کاربران این سیستم‌عامل‌ها فراهم می‌کند.
  • کاربر می تواند داده تولید کند و لیست فیلم های خود را ایجاد کند. این برنامه داده های کاربر را ذخیره و بازیابی می کند.
  • نماها، اجزا و مدل ها به وضوح با استفاده از الگوی Redux از هم جدا می شوند. جریان داده در اینجا یک طرفه است. می توان آن را به طور کامل کش، بازیابی و بازنویسی کرد.
  • این برنامه از اجزای اصلی SwiftUI، TabbedView، SegmentedControl، NavigationView، Form، Modal و غیره استفاده می کند. همچنین نماهای سفارشی، حرکات، UI/UX را ارائه می دهد.

توسعه برنامه در SwiftUI. بخش 1: Dataflow و Redux
در واقع، انیمیشن روان است، GIF کمی تند و ناگهانی است

کار بر روی اپلیکیشن به من تجربه زیادی داد و در کل تجربه مثبتی بود. من توانستم یک برنامه کاملا کاربردی بنویسم، در ماه سپتامبر آن را بهبود بخشیده و همزمان با انتشار iOS 13 در AppStore منتشر خواهم کرد.

Redux، BindableObject و EnvironmentObject

توسعه برنامه در SwiftUI. بخش 1: Dataflow و Redux

من در حال حاضر حدود دو سال است که با Redux کار می کنم، بنابراین نسبتاً در آن مهارت دارم. به طور خاص، من از آن در قسمت جلویی استفاده می کنم واکنش نشان می دهند وب سایت، و همچنین برای توسعه برنامه های کاربردی iOS (Swift) و اندروید (Kotlin) بومی.

من هرگز از انتخاب Redux به عنوان معماری جریان داده برای ساخت یک برنامه SwiftUI پشیمان نشدم. چالش‌برانگیزترین بخش هنگام استفاده از Redux در یک برنامه UIKit، کار با فروشگاه و دریافت و بازیابی داده‌ها و نگاشت آن‌ها به نماها/کامپوننت‌های شما است. برای انجام این کار، مجبور شدم نوعی کتابخانه از اتصال دهنده ها (با استفاده از ReSwift و ReKotlin) ایجاد کنم. خوب کار می کند، اما کد بسیار زیادی دارد. متأسفانه (هنوز) منبع باز نیست.

خبر خوب! تنها چیزهایی که باید در مورد SwiftUI نگران باشید - اگر قصد دارید از Redux استفاده کنید - فروشگاه ها، ایالت ها و کاهش دهنده ها هستند. تعامل با فروشگاه به لطف @EnvironmentObject توسط SwiftUI کاملاً مراقبت می شود. بنابراین، فروشگاه با یک BindableObject شروع می شود.

من یک بسته Swift ساده ایجاد کردم، SwiftUIFlux، که استفاده اولیه از Redux را فراهم می کند. در مورد من این بخشی از MovieSwiftUI است. من هم همینطور یک آموزش گام به گام نوشت، که به شما در استفاده از این کامپوننت کمک می کند.

چگونه کار می کند؟

final public class Store<State: FluxState>: BindableObject {
    public let willChange = PassthroughSubject<Void, Never>()
        
    private(set) public var state: State
    
    private func _dispatch(action: Action) {
        willChange.send()
        state = reducer(state, action)
    }
}

هر بار که یک عمل را فعال می کنید، گیربکس را فعال می کنید. اقدامات را با توجه به وضعیت فعلی برنامه ارزیابی می کند. سپس یک وضعیت تغییر یافته جدید مطابق با نوع عمل و داده ها برمی گرداند.

خوب، از آنجایی که store یک BindableObject است، زمانی که مقدار آن با استفاده از ویژگی willChange ارائه شده توسط PassthroughSubject تغییر کند، SwiftUI را مطلع می کند. این به این دلیل است که BindableObject باید یک PublisherType ارائه دهد، اما اجرای پروتکل مسئول مدیریت آن است. به طور کلی، این یک ابزار بسیار قدرتمند از اپل است. بر این اساس، در چرخه رندر بعدی، SwiftUI به رندر بدنه نماها مطابق با تغییر حالت کمک می کند.

در واقع، این تمام قلب و جادوی SwiftUI است. اکنون، در هر نمائی که مشترک یک وضعیت باشد، نمای با توجه به اینکه چه داده هایی از حالت دریافت شده و چه چیزی تغییر کرده است، ارائه می شود.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
 
    var window: UIWindow?
 
 
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            
            let controller = UIHostingController(rootView: HomeView().environmentObject(store))
            window.rootViewController = controller
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}
 
struct CustomListCoverRow : View {
    @EnvironmentObject var store: Store<AppState>
    
    let movieId: Int
    var movie: Movie! {
        return store.state.moviesState.movies[movieId]
    }
    
    var body: some View {
        HStack(alignment: .center, spacing: 0) {
            Image(movie.poster)
        }.listRowInsets(EdgeInsets())
    }
}

هنگام شروع برنامه، Store به عنوان یک EnvironmentObject تزریق می شود و سپس با استفاده از @EnvironmentObject در هر نمای قابل دسترسی است. هیچ جریمه عملکردی وجود ندارد زیرا ویژگی های مشتق شده به سرعت بازیابی یا از حالت برنامه محاسبه می شوند.

در صورت تغییر پوستر فیلم، کد بالا تصویر را تغییر می دهد.

و این در واقع فقط با یک خط انجام می شود که با کمک آن نماها به حالت متصل می شوند. اگر با ReSwift در iOS یا حتی کار کرده اید اتصال با React، جادوی SwiftUI را درک خواهید کرد.

اکنون می‌توانید اقدام را فعال کرده و وضعیت جدید را منتشر کنید. در اینجا یک مثال پیچیده تر است.

struct CustomListDetail : View {
    @EnvironmentObject var store: Store<AppState>
 
    let listId: Int
    
    var list: CustomList {
        store.state.moviesState.customLists[listId]!
    }
    
    var movies: [Int] {
        list.movies.sortedMoviesIds(by: .byReleaseDate, state: store.state)
    }
    
    var body: some View {
        List {
            ForEach(movies) { movie in
                NavigationLink(destination: MovieDetail(movieId: movie).environmentObject(self.store)) {
                    MovieRow(movieId: movie, displayListImage: false)
                }
            }.onDelete { (index) in
               self.store.dispatch(action: MoviesActions.RemoveMovieFromCustomList(list: self.listId, movie: self.movies[index.first!]))
            }
        }
    }
}

در کد بالا، من از عمل .onDelete از SwiftUI برای هر IP استفاده می کنم. این به ردیف موجود در لیست اجازه می‌دهد تا کش رفتن عادی iOS را برای حذف نمایش دهد. بنابراین هنگامی که کاربر دکمه حذف را لمس می کند، اقدام مربوطه را آغاز می کند و فیلم را از لیست حذف می کند.

خوب، از آنجایی که ویژگی لیست از حالت BindableObject مشتق شده است و به عنوان یک EnvironmentObject تزریق می شود، SwiftUI لیست را به روز می کند زیرا ForEach با ویژگی محاسبه شده فیلم ها مرتبط است.

در اینجا بخشی از کاهش دهنده MoviesState آمده است:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    
    // other actions.
    
    case let action as MoviesActions.AddMovieToCustomList:
        state.customLists[action.list]?.movies.append(action.movie)
        
    case let action as MoviesActions.RemoveMovieFromCustomList:
        state.customLists[action.list]?.movies.removeAll{ $0 == action.movie }
        
    default:
        break
    }
    return state
}

کاهنده زمانی اجرا می شود که شما یک اکشن را ارسال می کنید و حالت جدیدی را برمی گردانید، همانطور که در بالا گفته شد.

من هنوز وارد جزئیات نمی‌شوم - چگونه SwiftUI واقعاً می‌داند چه چیزی را نمایش دهد. برای درک عمیق تر این، ارزش دارد مشاهده جلسه WWDC در جریان داده در SwiftUI. همچنین به تفصیل توضیح می دهد که چرا و چه زمانی از آن استفاده کنید دولت، @Binding، ObjectBinding و EnvironmentObject.

Skillbox توصیه می کند:

منبع: www.habr.com

اضافه کردن نظر