تطوير التطبيق على SwiftUI. الجزء 1: تدفق البيانات والإعادة

تطوير التطبيق على SwiftUI. الجزء 1: تدفق البيانات والإعادة

بعد المشاركة في جلسة حالة الاتحاد في WWDC 2019 ، قررت دراسة SwiftUI بالتفصيل. لقد قضيت الكثير من الوقت في العمل معه وبدأت الآن في تطوير تطبيق حقيقي يمكن أن يكون مفيدًا لمجموعة واسعة من المستخدمين.

أسميته MovieSwiftUI - إنه تطبيق للبحث عن الأفلام الجديدة والقديمة ، بالإضافة إلى تجميعها في مجموعة باستخدام واجهة برمجة تطبيقات TMDB. لطالما أحببت الأفلام وحتى أنشأت شركة تعمل في هذا المجال ، على الرغم من ذلك منذ وقت طويل. كان من الصعب وصف الشركة بأنها رائعة ، لكن التطبيق - نعم!

نذكر: لجميع قراء "Habr" - خصم 10 روبل عند التسجيل في أي دورة Skillbox باستخدام رمز "Habr" الترويجي.

يوصي Skillbox بما يلي: دورة تعليمية عبر الإنترنت "مهنة مطور جافا".

إذن ماذا تفعل MovieSwiftUI؟

  • يتفاعل مع API - هذا ما يفعله أي تطبيق حديث تقريبًا.
  • تحميل البيانات غير المتزامنة عند الطلب ويوزع JSON في نموذج Swift باستخدام قابل للتشفير.
  • يظهر الصور التي تم تحميلها عند الطلب وتخزينها مؤقتًا.
  • يوفر هذا التطبيق لأنظمة iOS و iPadOS و macOS أفضل تجربة مستخدم لمستخدمي نظام التشغيل هؤلاء.
  • يمكن للمستخدم إنشاء البيانات وإنشاء قوائم الأفلام الخاصة به. يحفظ التطبيق ويستعيد بيانات المستخدم.
  • يتم فصل المشاهدات والمكونات والنماذج بوضوح باستخدام نمط Redux. تدفق البيانات هنا أحادي الاتجاه. يمكن تخزينها بالكامل واستعادتها والكتابة عليها.
  • يستخدم التطبيق المكونات الأساسية لـ SwiftUI و TabbedView و SegmentedControl و NavigationView و Form و Modal وما إلى ذلك. كما يوفر طرق عرض مخصصة ، وإيماءات ، وواجهة مستخدم / UX.

تطوير التطبيق على SwiftUI. الجزء 1: تدفق البيانات والإعادة
في الواقع ، كانت الرسوم المتحركة سلسة ، واتضح أن GIF كان مضطربًا بعض الشيء

لقد منحني العمل على التطبيق الكثير من الخبرة وهي عمومًا تجربة إيجابية. تمكنت من كتابة تطبيق يعمل بكامل طاقته ، في سبتمبر سأقوم بتحسينه ووضعه في AppStore ، بالتزامن مع إصدار iOS 13.

إحياء ، BindableObject و EnvironmentObject

تطوير التطبيق على SwiftUI. الجزء 1: تدفق البيانات والإعادة

أعمل مع Redux منذ حوالي عامين ، لذا فأنا جيد نسبيًا في ذلك. على وجه الخصوص ، أستخدمه في الواجهة الأمامية لـ رد فعل بالإضافة إلى تطوير تطبيقات iOS (Swift) و Android (Kotlin) أصلية.

لم أندم أبدًا على اختيار Redux باعتباره بنية تدفق البيانات الخاصة بي لبناء تطبيق SwiftUI. الجزء الأصعب من استخدام Redux في تطبيق UIKit هو التعامل مع المتجر والحصول على البيانات واستردادها وتعيينها حسب طرق العرض / المكونات الخاصة بك. للقيام بذلك ، كان علي إنشاء نوع من مكتبة الموصلات (على ReSwift و ReKotlin). يعمل بشكل جيد ، لكن هناك الكثير من التعليمات البرمجية. لسوء الحظ ، فهو ليس (حتى الآن) مفتوح المصدر.

أخبار جيدة! الشيء الوحيد الذي يجب أن تقلق بشأنه مع SwiftUI - إذا كنت تخطط لاستخدام Redux - هو المتجر والحالات والمخفضات. تم الاستيلاء على التفاعل مع المتجر بالكامل بواسطة SwiftUI بفضلEnvironmentObject. لذلك يبدأ المتجر بـ BindableObject.

لقد قمت بإنشاء حزمة Swift بسيطة ، سويفت، والذي يوفر الاستخدام الأساسي لـ 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 ، فسيقوم بإخطار SwiftUI عندما تتغير قيمته باستخدام خاصية willChange التي يوفرها PassthroughSubject. هذا لأن BindableObject يجب أن يوفر PublisherType ، لكن تطبيق البروتوكول مسؤول عن إدارته. بشكل عام ، هذه أداة قوية جدًا من Apple. وفقًا لذلك ، في دورة التجسيد التالية ، ستساعد 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())
    }
}

يتم حقن المتجر ككائن 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 عادي للحذف. لذلك عندما ينقر المستخدم على زر الحذف ، فإنه يقوم بتشغيل الإجراء المقابل ويزيل الفيلم من القائمة.

حسنًا ، نظرًا لأن خاصية list مشتقة من حالة 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

إضافة تعليق