Interaksi SwiftUI dengan Redux

gambar

Halo semuanya. Pada artikel ini kita akan berbicara tentang kerangka kerja SwiftUI bersama dengan Redux, bundel ini memungkinkan kita membuat aplikasi dengan cepat dan mudah. SwiftUI digunakan untuk membuat antarmuka pengguna gaya deklaratif, tidak seperti UIKit . Redux, pada gilirannya, berfungsi untuk mengontrol keadaan aplikasi.

Negara adalah konsep dasar dalam SwiftUI dan Redux. Dalam kasus kami, ini bukan hanya kata kunci, tetapi juga entitas yang menghubungkan mereka dan memungkinkan mereka untuk bekerja sama dengan sangat baik. Pada artikel ini saya akan mencoba menunjukkan bahwa tesis di atas adalah benar, jadi mari kita mulai!

Sebelum kita terjun ke dalam penulisan kode, mari kita pahami apa itu Redux dan terdiri dari apa.

Redux adalah pustaka sumber terbuka untuk mengelola keadaan aplikasi. Paling sering digunakan bersama dengan React atau Angular untuk mengembangkan sisi klien. Berisi sejumlah alat untuk menyederhanakan transfer data penyimpanan secara signifikan melalui konteks. Pembuat: Daniil Abramov dan Andrew Clark.

Bagi saya, Redux bukan hanya perpustakaan, itu sudah sesuatu yang lebih, saya mengaitkannya dengan keputusan arsitektur yang menjadi dasar aplikasi tersebut. Terutama karena aliran data searah.

Aliran multidireksional atau searah


Untuk menjelaskan apa yang saya maksud dengan aliran data, saya akan memberikan contoh berikut. Aplikasi yang dibuat menggunakan VIPER mendukung aliran data multi arah antar modul:

gambar

Redux, pada gilirannya, adalah aliran data searah dan paling mudah dijelaskan berdasarkan komponen penyusunnya.

gambar

Mari kita bicara sedikit lebih rinci tentang setiap komponen Redux.

Negara adalah satu-satunya sumber kebenaran yang berisi semua informasi yang diperlukan untuk aplikasi kita.

Tindakan adalah niat untuk mengubah keadaan. Dalam kasus kami, ini adalah penghitungan yang berisi informasi baru yang ingin kami tambahkan atau ubah di Status saat ini.

PeredamMerupakan fungsi yang mengambil Tindakan dan Status saat ini sebagai parameter dan mengembalikan Status baru. Ini adalah satu-satunya cara untuk membuatnya. Perlu juga dicatat bahwa fitur ini harus bersih.

Store adalah objek yang berisi Status dan menyediakan semua alat yang diperlukan untuk memperbaruinya.

Saya percaya bahwa teorinya cukup, mari kita lanjutkan ke latihan.

Implementasi redux


Salah satu cara termudah untuk mengenal alat adalah mulai menggunakannya, seperti yang dikatakan oleh guru pemrograman saya, jika Anda ingin belajar bahasa pemrograman, tulis aplikasi di atasnya. Jadi mari kita buat aplikasi kecil, biarlah itu buku harian pelatihan sederhana, itu hanya akan memiliki empat opsi, yang pertama adalah menampilkan daftar latihan, yang kedua adalah menambahkan latihan yang selesai, yang ketiga adalah menghapus dan yang keempat adalah untuk memilah latihan. Aplikasi yang cukup sederhana, tetapi pada saat yang sama akan memungkinkan kita untuk berkenalan dengan Redux dan SwiftUI.

Buat proyek bersih di Xcode, beri nama WorkoutsDiary, dan yang paling penting pilih SwiftUI untuk User Interface, karena kita akan menggunakan SwiftUI untuk membuat antarmuka pengguna kami.

Setelah membuat proyek. Buat struktur Latihan yang akan bertanggung jawab atas latihan yang kita selesaikan.

import Foundation

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

Seperti yang Anda lihat, tidak ada yang gila dengan struktur ini, bidang id diperlukan untuk mematuhi protokol yang dapat diidentifikasi, dan bidang kompleksitas hanyalah enum dengan definisi berikut:

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

Sekarang kita memiliki semua yang kita butuhkan untuk mulai menerapkan Redux, mari kita mulai dengan membuat Negara.

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

Negara adalah struktur sederhana yang berisi dua bidang: latihan dan sortType . Yang pertama adalah daftar latihan, dan yang kedua adalah bidang opsional yang menentukan bagaimana daftar diurutkan.

SortType adalah enumerasi yang didefinisikan sebagai berikut:

enum SortType {
    case distance
    case complexity
}

Untuk kesederhanaan, kami akan mengurutkan berdasarkan jarak dan kesulitan dalam urutan menurun, yaitu semakin tinggi kompleksitas pelatihan kami, semakin tinggi akan ditampilkan dalam daftar kami. Perlu dicatat bahwa sortType adalah tipe opsional dan bisa nihil, yang berarti bahwa daftar tersebut tidak diurutkan saat ini.

Kami akan melanjutkan implementasi komponen kami. Mari kita membuat Action

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

Seperti yang bisa kita lihat, Aksi adalah enumerasi dengan tiga kasus yang memberi kita kemampuan untuk memanipulasi Negara kita .

  • addWorkout (_ workout: Workout) cukup menambahkan latihan yang dilewatkan sebagai parameter.
  • removeWorkout (at: IndexSet) menghapus item pada indeks yang ditentukan.
  • sort (menurut: SortType) mengurutkan daftar pelatihan berdasarkan jenis sortir yang ditentukan.

Mari kita membuat salah satu komponen paling kompleks, ini adalah 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
}


Fungsi yang kami tulis cukup sederhana dan berfungsi sebagai berikut:

  1. Salinan Negara saat ini untuk bekerja dengannya.
  2. Berdasarkan Action , kami memperbarui Negara yang kami salin .
  3. Kembali diperbarui Negara

Perlu dicatat bahwa fungsi di atas adalah fungsi murni, dan itulah yang ingin kami capai! Suatu fungsi harus memenuhi dua syarat agar dianggap “murni”:

  • Setiap kali, fungsi mengembalikan hasil yang sama ketika dipanggil dengan kumpulan data yang sama.
  • Tidak ada efek samping.

Elemen Redux yang terakhir hilang adalah Store , jadi mari kita implementasikan untuk aplikasi kita.

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

Dalam implementasi objek Store , kami menggunakan semua keuntungan dari protokol ObservableObject , yang memungkinkan kami untuk mengecualikan penulisan sejumlah besar kode templat atau penggunaan kerangka kerja pihak ketiga. Properti State hanya-baca dan menggunakan pembungkus properti @Published , yang berarti bahwa setiap kali diubah, SwiftUI akan menerima pemberitahuan. Metode init mengambil status awal sebagai parameter dengan nilai default yang diberikan dalam bentuk array kosong elemen Workout. Fungsi pengiriman adalah satu-satunya cara untuk memperbarui keadaan: menggantikan kondisi saat ini dengan yang baru yang dibuat oleh fungsi peredam , berdasarkan padaTindakan , yang dilewatkan sebagai parameter.

Sekarang kita telah menerapkan semua komponen Redux, kita dapat mulai membuat antarmuka pengguna untuk aplikasi kita.

Implementasi aplikasi


Antarmuka pengguna aplikasi kita akan sangat sederhana. Dan itu akan terdiri dari dua layar kecil. Layar pertama dan utama adalah layar yang akan menampilkan daftar latihan. Layar kedua adalah layar tambah latihan. Juga, setiap elemen akan ditampilkan dalam warna tertentu, warnanya akan mencerminkan kompleksitas latihan. Sel-sel merah menunjukkan kesulitan tertinggi dari latihan, oranye bertanggung jawab atas kesulitan rata-rata dan hijau bertanggung jawab atas latihan yang paling mudah.

Kami akan mengimplementasikan antarmuka menggunakan kerangka kerja baru dari Apple yang disebut SwiftUI. SwiftUI datang untuk menggantikan UIKit yang kita kenal. SwiftUI pada dasarnya berbeda dari UIKit, terutama karena itu adalah pendekatan deklaratif untuk menulis elemen UI dengan kode. Pada artikel ini, saya tidak akan menyelidiki semua seluk-beluk SwiftUI dan saya berasumsi bahwa Anda sudah memiliki pengalaman dengan SwiftUI. Jika Anda tidak memiliki pengetahuan tentang SwiftUI, saya menyarankan Anda untuk memperhatikan dokumentasi dari Apple, yaitu, lihat beberapa tutorial lengkap mereka dengan penambahan langkah demi langkah dan tampilan interaktif dari hasil yang dilihat. Ada juga tautan ke proyek contoh. Tutorial-tutorial ini akan memungkinkan Anda untuk terjun cepat ke dunia deklaratif SwiftUI.

Perlu juga dicatat bahwa SwiftUI belum siap untuk proyek produksi, masih terlalu muda dan lebih dari setahun akan berlalu sebelum dapat digunakan dengan cara ini. Juga, jangan lupa bahwa itu hanya mendukung versi iOS 13.0+. Tetapi perlu dicatat bahwa SwiftUI akan bekerja pada semua platform Apple, yang merupakan keuntungan besar dibandingkan UIKit!

Mari kita mulai implementasi dari layar utama aplikasi kita. Buka file ContentView.swift, ubah kode saat ini menjadi ini.

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

Tampilan Konten adalah tampilan standar di SwiftUI. Bagian terpenting - dari sudut pandang saya - adalah baris kode yang berisi variabel toko. Kami akan membuat @EnvironmentObject. Ini akan memungkinkan kami untuk menggunakan data dari Store di mana pun diperlukan, dan di samping itu, secara otomatis akan memperbarui tampilan kami jika data diubah. Ini adalah sesuatu seperti Singleton untuk Toko kami.

@EnvironmentObject var store: Store

Perlu diperhatikan juga baris kode berikut:

@State private var isAddingMode: Bool = false

NegaraAdalah pembungkus yang bisa kita gunakan untuk menunjukkan keadaan Tampilan. SwiftUI akan menyimpannya dalam memori internal khusus di luar struktur View. Hanya Tampilan yang ditautkan yang dapat mengaksesnya. Begitu nilai propertiNegara perubahan, SwiftUI membangun kembali Lihat ke akun untuk perubahan negara.

gambar

Kemudian kita akan pergi ke file SceneDelegate.swift dan menambahkan kode ke metode:

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

Dengan cara yang sama, @EnvironmentObject apa pun dapat diteruskan ke representasi anak di seluruh aplikasi, dan semua ini dimungkinkan berkat Lingkungan. Variabel IsAddingMode ditandaiNegaradan menunjukkan apakah tampilan sekunder ditampilkan atau tidak. Variabel toko secara otomatis diwarisi oleh WorkoutListView , dan kami tidak perlu meneruskannya secara eksplisit, tetapi kami perlu melakukan ini untuk AddWorkoutView , karena disajikan dalam bentuk lembar yang bukan anak dari ContentView .

Sekarang buat WorkoutListView yang akan diwarisi dari View. Buat file cepat baru yang disebut 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())
        }
    }
}

Lihat, yang menggunakan elemen Daftar kontainer untuk menampilkan daftar latihan. Fungsi onDelete digunakan untuk menghapus latihan dan menggunakan tindakan removeWorkout , yang dilakukan menggunakan fungsi pengiriman yang disediakan oleh toko . Untuk menampilkan latihan dalam daftar, gunakan WorkoutView.

Buat file WorkoutView.swift lain yang akan bertanggung jawab untuk menampilkan item kami dalam daftar.

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

Pandangan ini mengambil objek pelatihan sebagai parameter dan dikonfigurasikan berdasarkan propertinya.

Untuk menambahkan item baru ke daftar, Anda harus mengubah parameter isAddingMode menjadi true untuk menampilkan AddWorkoutView . Tanggung jawab ini terletak pada AddButton .

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

AddButton juga layak dimasukkan ke file terpisah.

Tampilan ini adalah tombol sederhana yang telah diekstraksi dari ContentView utama untuk struktur dan pemisahan kode yang lebih baik.

Buat tampilan untuk menambahkan latihan baru. Buat file AddWorkoutView.swift baru :

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

Ini adalah pengontrol yang cukup besar yang, seperti pengontrol lainnya, berisi variabel toko. Ini juga berisi variabel nameText, distanceText, complexField, dan isAddingMode . Tiga variabel pertama diperlukan untuk menghubungkan TextField, Picker, DatePicker , yang dapat dilihat pada layar ini. Bilah navigasi memiliki dua elemen. Tombol pertama adalah tombol yang menutup layar tanpa menambahkan latihan baru, dan yang terakhir menambahkan latihan baru ke daftar, yang dicapai dengan mengirimkan tindakan addWorkout. Tindakan ini juga menutup layar tambah latihan baru.

gambar

Last but not least adalah 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()
        }
    }
}

Tampilan ini terdiri dari dua tombol yang bertanggung jawab untuk menyortir daftar latihan dan untuk mengaktifkan atau menonaktifkan mode pengeditan daftar latihan kami. Tindakan sortir disebut menggunakan fungsi pengiriman, yang dapat kita panggil melalui properti store.

gambar

Hasil


Aplikasi sudah siap dan harus bekerja persis seperti yang diharapkan. Mari kita coba kompilasi dan jalankan.

temuan


Redux dan SwiftUI bekerja dengan sangat baik. Kode yang ditulis menggunakan alat ini mudah dipahami dan dapat diatur dengan baik. Aspek lain yang baik dari solusi ini adalah kemampuan uji kode yang sangat baik. Namun, solusi ini bukan tanpa kekurangan, salah satunya adalah jumlah besar memori yang digunakan oleh aplikasi ketika Keadaan aplikasi sangat kompleks, dan kinerja aplikasi mungkin tidak ideal dalam beberapa skenario tertentu, karena semua Tampilan di SwiftUI diperbarui saat membuat Negara baru. Kekurangan ini dapat berdampak besar pada kualitas aplikasi dan interaksi pengguna, tetapi jika kita mengingatnya dan menyiapkan keadaan dengan cara yang masuk akal, dampak negatifnya dapat dengan mudah diminimalisasi atau bahkan dihindari.

Anda dapat mengunduh atau melihat kode sumber proyek.di sini .

Saya harap Anda menyukai artikel ini dan Anda belajar sesuatu yang baru untuk diri Anda sendiri, sampai jumpa lagi. Selanjutnya akan lebih menarik.

All Articles