تفاعل SwiftUI مع Redux

صورة

تحية للجميع. في هذه المقالة سنتحدث عن إطار عمل SwiftUI بالاشتراك مع Redux ، تتيح لنا هذه الحزمة إنشاء تطبيقات بسرعة وسهولة. يتم استخدام SwiftUI لإنشاء واجهة مستخدم بأسلوب إعلاني ، على عكس UIKit . يعمل Redux بدوره على التحكم في حالة التطبيق.

الدولة هي مفهوم أساسي في SwiftUI و Redux. في حالتنا ، هذه ليست مجرد كلمة طنانة ، ولكنها أيضًا كيان يربطها ويسمح لها بالعمل معًا بشكل جيد للغاية. في هذه المقالة سأحاول أن أثبت أن الأطروحة أعلاه صحيحة ، فلنبدأ!

قبل أن نتعمق في كتابة التعليمات البرمجية ، دعنا نفهم أولاً ما هو Redux وما يتكون منه.

Redux هي مكتبة مفتوحة المصدر لإدارة حالة التطبيق. غالبًا ما يتم استخدامه مع React أو Angular لتطوير جانب العميل. يحتوي على عدد من الأدوات لتبسيط نقل بيانات التخزين بشكل كبير من خلال السياق. التأليف: دانييل أبراموف وأندرو كلارك.

بالنسبة لي ، Redux ليست مجرد مكتبة ، إنها بالفعل شيء أكثر من ذلك ، وأنا أعزوها إلى القرارات المعمارية التي يستند إليها التطبيق. في المقام الأول بسبب تدفق البيانات أحادي الاتجاه.

تدفق متعدد الاتجاهات أو أحادي الاتجاه


لشرح ما أعنيه بتدفق البيانات ، سأعطي المثال التالي. تطبيق تم إنشاؤه باستخدام VIPER يدعم دفق بيانات متعدد الاتجاهات بين الوحدات:

صورة

Redux ، بدوره ، هو دفق بيانات أحادي الاتجاه ويسهل شرحه بناءً على مكوناته المكونة.

صورة

لنتحدث قليلاً عن كل مكون من مكونات Redux.

الدولة هي المصدر الوحيد للحقيقة الذي يحتوي على جميع المعلومات اللازمة لتطبيقنا.

العمل هو نية تغيير الدولة. في حالتنا ، هذا تعداد يحتوي على معلومات جديدة نريد إضافتها أو تغييرها في الحالة الحالية.

المخفضهي وظيفة تأخذ الإجراء والحالة الحالية كمعلمات وتعيد دولة جديدة. هذه هي الطريقة الوحيدة لإنشائه. تجدر الإشارة أيضًا إلى أن هذه الميزة يجب أن تكون نظيفة.

المتجر هو كائن يحتوي على State ويوفر جميع الأدوات اللازمة لتحديثه.

أعتقد أن النظرية كافية ، دعنا ننتقل إلى الممارسة.

تنفيذ Redux


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

قم بإنشاء مشروع نظيف في Xcode ، ومنحه اسم WorkoutsDiary ، والأهم من ذلك تحديد SwiftUI لواجهة المستخدم ، حيث سنستخدم SwiftUI لإنشاء واجهة المستخدم الخاصة بنا.

بعد إنشاء المشروع. إنشاء هيكل تجريب سيكون مسؤولاً عن التمرين الذي أكملناه.

import Foundation

struct Workout: Identifiable {
    let id: UUID = .init()
    let name: String
    let distance: String
    let date: Date
    let complexity: Complexity
}

كما ترى ، لا يوجد شيء مجنون في هذا الهيكل ، حقل الهوية مطلوب للامتثال لبروتوكول التعريف ، ومجال التعقيد هو مجرد تعداد مع التعريف التالي:

enum Complexity: Int {
    case low
    case medium
    case high
}

الآن بعد أن أصبح لدينا كل ما نحتاجه لبدء تنفيذ Redux ، فلنبدأ بإنشاء دولة.

struct AppState {
    var workouts: [Workout]
    var sortType: SortType?
}

State هي بنية بسيطة تحتوي على حقلين: التدريبات و sortType . الأول هو قائمة التدريبات ، والثاني هو حقل اختياري يحدد كيفية فرز القائمة.

SortType عبارة عن تعداد يتم تعريفه على النحو التالي:

enum SortType {
    case distance
    case complexity
}

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

سنستمر في تنفيذ مكوناتنا. لنقم بإنشاء إجراء

enum Action {
    case addWorkout(_ workout: Workout)
    case removeWorkout(at: IndexSet)
    case sort(by: SortType)
}

وكما نرى، العمل هو تعداد مع ثلاث حالات التي تعطينا القدرة على التلاعب لدينا دولة .

  • addWorkout (_ تمرين: تمرين) ببساطة يضيف تمرين يتم تمريره كمعلمة.
  • removeWorkout (في: IndexSet) يزيل العنصر في الفهرس المحدد.
  • فرز (حسب: SortType) يفرز قائمة التدريب حسب نوع الفرز المحدد.

لنقم بإنشاء أحد أكثر المكونات تعقيدًا ، وهذا هو Reducer :

func reducer(state: AppState, action: Action) -> AppState {
    var state = state
    
    switch action {
    case .addWorkout(let workout):
        state.workouts.append(workout)
        
    case .removeWorkout(let indexSet):
        state.workouts.remove(atOffsets: indexSet)
    
    case .sort(let type):
        switch type {
        case .distance:
            state.workouts.sort { $0.distance > $1.distance }
            state.sortType = .distance
        case .complexity:
            state.workouts.sort { $0.complexity.rawValue > $1.complexity.rawValue }
            state.sortType = .complexity
        }
    }
    return state
}


الوظيفة التي كتبناها بسيطة للغاية وتعمل على النحو التالي:

  1. نسخ الراهنة الدولة للعمل معها.
  2. بناء على العمل ، ونحن لدينا تحديث نسخها الدولة .
  3. إعادة تحديث الدولة

من الجدير بالذكر أن الوظيفة المذكورة أعلاه هي وظيفة نقية ، وهذا ما أردنا تحقيقه! يجب أن تستوفي الوظيفة شرطين حتى تعتبر "نقية":

  • في كل مرة ، تقوم الدالة بإرجاع نفس النتيجة عندما يتم استدعاؤها بنفس مجموعة البيانات.
  • ليس هناك أي آثار جانبية.

آخر عنصر Redux مفقود هو المتجر ، لذلك دعنا ننفذه لتطبيقنا .

final class Store: ObservableObject {
     @Published private(set) var state: AppState
     
     init(state: AppState = .init(workouts: [Workout]())) {
         self.state = state
     }
     
     public func dispatch(action: Action) {
         state = reducer(state: state, action: action)
     }
 }

في تطبيقات كائن المتجر ، نستخدم جميع مزايا بروتوكول ObservableObject ، الذي يسمح لنا باستبعاد كتابة كمية كبيرة من كود القالب أو استخدام أطر عمل الطرف الثالث. خاصية الولاية هي للقراءة فقط وتستخدم غلاف خاصية Published ، مما يعني أنه كلما تم تغييرها ، سيتلقى SwiftUI إشعارات. يأخذ أسلوب init حالة أولية كمعلمة ذات قيمة افتراضية معينة في شكل صفيف فارغ من عناصر التمرين. وظيفة الإرسال هي الطريقة الوحيدة لتحديث الحالة: فهي تستبدل الحالة الحالية بالحالة الجديدة التي تم إنشاؤها بواسطة وظيفة المخفض ، بناءً علىالإجراء ، الذي يتم تمريره كمعامل.

الآن بعد أن قمنا بتنفيذ جميع مكونات Redux ، يمكننا البدء في إنشاء واجهة مستخدم لتطبيقنا.

تنفيذ التطبيق


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

سنقوم بتنفيذ الواجهة باستخدام إطار عمل جديد من Apple يسمى SwiftUI. يأتي SwiftUI ليحل محل UIKit المألوف لدينا. يختلف SwiftUI اختلافًا جوهريًا عن UIKit ، في المقام الأول من حيث أنه نهج تعريفي لكتابة عناصر واجهة المستخدم مع التعليمات البرمجية. في هذه المقالة ، لن أخوض في جميع تعقيدات SwiftUI وأفترض أن لديك بالفعل خبرة مع SwiftUI. إذا لم تكن على دراية بـ SwiftUI ، أنصحك بالاهتمام بالوثائق من Apple ، وبالتحديد ، انظر إلى العديد من البرامج التعليمية الكاملة الخاصة بهم مع إضافة خطوة بخطوة وعرض تفاعلي للنتيجة المعروضة. هناك أيضا روابط لأمثلة المشاريع. ستتيح لك هذه البرامج التعليمية الغوص بسرعة في عالم SwiftUI التعريفي.

من الجدير بالذكر أيضًا أن SwiftUI ليست جاهزة بعد لمشاريع الإنتاج ، إنها صغيرة جدًا وستمر أكثر من عام قبل أن يتم استخدامها بهذه الطريقة. أيضًا ، لا تنس أنه يدعم فقط إصدارات iOS 13.0+. ولكن من الجدير بالذكر أيضًا أن SwiftUI ستعمل على جميع أنظمة Apple الأساسية ، والتي تعد ميزة كبيرة على UIKit!

لنبدأ التنفيذ من الشاشة الرئيسية لتطبيقنا. انتقل إلى الملف ContentView.swift وقم بتغيير الرمز الحالي إلى هذا.

struct ContentView: View {
    @EnvironmentObject var store: Store
    @State private var isAddingMode: Bool = false
    
    var body: some View {
        NavigationView {
            WorkoutListView()
                .navigationBarTitle("Workouts diary", displayMode: .inline)
                .navigationBarItems(
                    leading: AddButton(isAddingMode: self.$isAddingMode),
                    trailing: TrailingView()
                )
        }
        .sheet(isPresented: $isAddingMode) {
            AddWorkoutView(isAddingMode: self.$isAddingMode)
                .environmentObject(self.store)
        }
    }
}

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

@EnvironmentObject var store: Store

تجدر الإشارة أيضًا إلى سطر التعليمات البرمجية التالي:

@State private var isAddingMode: Bool = false

حالةعبارة عن برنامج تضمين يمكننا استخدامه للإشارة إلى حالة العرض. سوف يقوم SwiftUI بتخزينه في ذاكرة داخلية خاصة خارج هيكل العرض. يمكن فقط الوصول إلى طريقة عرض مرتبطة. بمجرد قيمة الممتلكاتحالة التغييرات ، SwiftUI يعيد بناء العرض لحساب التغييرات في الحالة.

صورة

ثم سنذهب إلى ملف SceneDelegate.swift ونضيف الكود إلى الطريقة:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        let contentView = ContentView().environmentObject(Store())
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

بنفس الطريقة ، يمكن تمرير أي EnvironmentObject إلى أي تمثيل فرعي في التطبيق بأكمله ، وكل هذا ممكن بفضل البيئة. IsAddingMode متغير يتم وضع علامةحالةوتشير إلى ما إذا كان العرض الثانوي يتم عرضه أم لا. يتم توريث متغير المتجر تلقائيًا بواسطة WorkoutListView ، ولا نحتاج إلى تمريره بشكل صريح ، لكننا بحاجة إلى القيام بذلك لـ AddWorkoutView ، لأنه يتم تقديمه في شكل ورقة ليست تابعة لـ ContentView .

الآن قم بإنشاء WorkoutListView الذي سيرث من العرض. قم بإنشاء ملف سريع جديد يسمى WorkoutListView .

struct WorkoutListView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        List {
            ForEach(store.state.workouts) {
                WorkoutView(workout: $0)
            }
            .onDelete {
                self.store.dispatch(action: .removeWorkout(at: $0))
            }
            .listRowInsets(EdgeInsets())
        }
    }
}

عرض ، والذي يستخدم عنصر قائمة الحاوية لعرض قائمة التدريبات. تُستخدم وظيفة onDelete لحذف جلسة تمرينات رياضية وتستخدم الإجراء removeWorkout ، الذي يتم تنفيذه باستخدام وظيفة الإرسال التي يوفرها المتجر . لعرض التمرين في القائمة ، يتم استخدام WorkoutView.

قم بإنشاء ملف آخر WorkoutView.swift سيكون مسؤولاً عن عرض العنصر الخاص بنا في القائمة.

struct WorkoutView: View {
    let workout: Workout
    
    private var backgroundColor: Color {
        switch workout.complexity {
        case .low:
            return .green
        case .medium:
            return .orange
        case .high:
            return .red
        }
    }
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(workout.name)
                Text("Distance:" + workout.distance + "km")
                    .font(.subheadline)
            }
            Spacer()
            VStack(alignment: .leading) {
                Text(simpleFormat(workout.date))
            }
        }
        .padding()
        .background(backgroundColor)
    }
}

private extension WorkoutView {
    func simpleFormat(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyyy"
        dateFormatter.locale = .init(identifier: "en_GB")
        return dateFormatter.string(from: date)
    }
}

تأخذ طريقة العرض هذه كائن التدريب كمعلمة ويتم تكوينه بناءً على خصائصه.

لإضافة عنصر جديد إلى القائمة ، يجب تغيير المعلمة isAddingMode إلى true لعرض AddWorkoutView . تقع هذه المسؤولية على AddButton .

struct AddButton: View {
    @Binding var isAddingMode: Bool
    
    var body: some View {
        Button(action: { self.isAddingMode = true }) {
            Image(systemName: "plus")
        }
    }
}

يستحق AddButton أيضًا وضعه في ملف منفصل.

هذا العرض هو زر بسيط تم استخراجه من ContentView الرئيسي من أجل بنية أفضل وفصل التعليمات البرمجية.

قم بإنشاء طريقة عرض لإضافة تمرين جديد. قم بإنشاء ملف AddWorkoutView.swift جديد :

struct AddWorkoutView: View {
    @EnvironmentObject private var store: Store
    
    @State private var nameText: String = ""
    @State private var distanceText: String = ""
    @State private var complexityField: Complexity = .medium
    @State private var dateField: Date = Date()
    @Binding var isAddingMode: Bool
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $nameText)
                TextField("Distance", text: $distanceText)
                Picker(selection: $complexityField, label: Text("Complexity")) {
                    Text("Low").tag(Complexity.low)
                    Text("Medium").tag(Complexity.medium)
                    Text("High").tag(Complexity.high)
                }
                DatePicker(selection: $dateField, displayedComponents: .date) {
                    Text("Date")
                }
            }
            .navigationBarTitle("Workout Details", displayMode: .inline)
            .navigationBarItems(
                leading: Button(action: { self.isAddingMode = false }) {
                    Text("Cancel")
                },
                trailing: Button(action: {
                    let workout = Workout(
                        name: self.nameText,
                        distance: self.distanceText,
                        date: self.dateField,
                        complexity: self.complexityField
                    )
                    self.store.dispatch(action: .addWorkout(workout))
                    self.isAddingMode = false
                }) {
                    Text("Save")
                }
                .disabled(nameText.isEmpty)
            )
        }
    }
}

هذه وحدة تحكم كبيرة إلى حد ما ، مثل وحدات التحكم الأخرى ، تحتوي على متغير المتجر. كما يحتوي على المتغيرات nameText و DistanceText و complexityField و isAddingMode . تعتبر المتغيرات الثلاثة الأولى ضرورية لربط TextField و Picker و DatePicker ، والتي يمكن رؤيتها على هذه الشاشة. يحتوي شريط التنقل على عنصرين. الزر الأول هو زر يغلق الشاشة دون إضافة تمرين جديد ، ويضيف الأخير تمرينًا جديدًا إلى القائمة ، والذي يتحقق عن طريق إرسال إجراء addWorkout. يؤدي هذا الإجراء أيضًا إلى إغلاق شاشة إضافة تمرين جديد.

صورة

أخيرًا وليس آخرًا هو TrailingView .

struct TrailingView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        HStack(alignment: .center, spacing: 30) {
            Button(action: {
                switch self.store.state.sortType {
                case .distance:
                    self.store.dispatch(action: .sort(by: .distance))
                default:
                    self.store.dispatch(action: .sort(by: .complexity))
                }
            }) {
                Image(systemName: "arrow.up.arrow.down")
            }
            EditButton()
        }
    }
}

تتكون طريقة العرض هذه من زرين مسؤولين عن فرز قائمة التمرين وعن تنشيط أو إلغاء تنشيط وضع التحرير لقائمة التمرين. تسمى إجراءات الفرز باستخدام وظيفة الإرسال ، والتي يمكننا استدعاؤها من خلال خاصية المتجر.

صورة

نتيجة


التطبيق جاهز ويجب أن يعمل كما هو متوقع تمامًا. دعونا نحاول تجميعها وتشغيلها.

الموجودات


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

يمكنك تنزيل أو عرض شفرة المصدر للمشروع.هنا .

آمل أن تكون قد أحببت هذه المقالة وتعلمت شيئًا جديدًا لنفسك ، أراك قريبًا. علاوة على ذلك سيكون أكثر إثارة للاهتمام.

All Articles