SwiftUI与Redux的交互

图片

大家好。在本文中,我们将讨论与Redux结合使用SwiftUI框架,该捆绑包使我们能够快速轻松地创建应用程序。与UIKit不同,SwiftUI用于创建声明式用户界面。反过来,Redux用于控制应用程序的状态。

状态是SwiftUI和Redux中的基本概念。在我们的案例中,这不仅是一个时髦的词,而且是一个将它们连接起来并允许它们很好地协同工作的实体。在本文中,我将尝试证明上述论点是正确的,让我们开始吧!

在开始编写代码之前,让我们首先了解什么是Redux及其组成。

Redux是一个用于管理应用程序状态的开源库。最常与React或Angular结合使用以开发客户端。包含许多工具,可通过上下文大大简化存储数据的传输。创作者:丹尼尔·阿布拉莫夫和安德鲁·克拉克。

对我来说,Redux不仅是一个库,而且已经更多了,我将其归因于应用程序所基于的体系结构决策。主要是由于其单向数据流。

多向或单向流


为了解释数据流的含义,我将给出以下示例。使用VIPER创建的应用程序支持模块之间的多向数据流:

图片

Redux又是单向数据流,最容易根据其组成部分进行解释。

图片

让我们详细讨论每个Redux组件。

状态是包含我们应用程序所有必要信息的唯一真理来源。

行动是改变状态的意图。在我们的情况下,这是一个枚举,其中包含我们要在当前状态中添加或更改的新信息。

减速器是一个将Action和当前状态作为参数并返回新状态的函数。这是创建它的唯一方法。还值得注意的是,此功能应该干净。

Store是一个包含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
}

如您所见,此结构没有什么疯狂的,要求id字段符合Identifiable协议,而complex字段只是具有以下定义的枚举:

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

现在,我们拥有开始实施Redux所需的一切,让我们开始创建一个State。

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

国家是一个简单的结构,它包含两个字段:锻炼sortType第一个是锻炼列表,第二个是确定列表排序方式的可选字段。

SortType是一个枚举,定义如下:

enum SortType {
    case distance
    case complexity
}

为简单起见,我们将按距离和难度按降序排序,即培训的复杂性越高,列表中显示的内容就越高。值得注意的是sortType是一个可选类型,可以为nil,这意味着该列表目前未排序。

我们将继续实施我们的组件。让我们创建一个动作

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

如我们所见,行动是列举了三种情况的列举,这三种情况使我们能够操纵国家

  • addWorkout(_锻炼:锻炼)只是添加一个作为参数传递的锻炼。
  • removeWorkout(位于:IndexSet)删除指定索引处的项目。
  • sort(按: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元素是Store,所以让我们为应用程序实现它。

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

Store对象的实现中,我们利用了ObservableObject协议的所有优点,该协议使我们可以排除编写大量模板代码或使用第三方框架的麻烦。State属性是只读的,并使用@Published属性的包装器,这意味着无论何时更改,SwiftUI都会收到通知。 init方法采用初始状态作为参数,并具有一个空缺的Workout元素数组形式的给定默认值。调度功能是更新状态的唯一方法:基于以下功能它用reducer函数创建的新状态替换当前状态Action,它作为参数传递。

现在,我们已经实现了Redux的所有组件,我们可以开始为应用程序创建用户界面。

应用实施


我们应用程序的用户界面将非常简单。它将由两个小屏幕组成。第一个屏幕和主屏幕是显示锻炼列表的屏幕。第二个屏幕是添加锻炼屏幕。同样,每个元素将以某种颜色显示,该颜色将反映锻炼的复杂性。红细胞表示锻炼的最高难度,橙色代表平均难度,绿色代表最简单的锻炼。

我们将使用Apple的称为SwiftUI的新框架来实现该接口。 SwiftUI取代了我们熟悉的UIKit。 SwiftUI与UIKit根本不同,主要在于它是一种使用代码编写UI元素的声明方法。在本文中,我不会深入研究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中的标准视图。从我的角度来看,最重要的部分是包含store变量的代码行。我们将创建@EnvironmentObject。这将使我们能够在需要的地方使用来自商店的数据,此外,如果数据发生更改,它将自动更新我们的视图。这就像是我们商店的Singleton。

@EnvironmentObject var store: Store

还应注意以下代码行:

@State private var isAddingMode: Bool = false

是一个包装器,可用于指示View的状态。SwiftUI会将其存储在View结构外部的特殊内部存储器中。只有链接的视图可以访问它。一旦属性值 更改后,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变量标记并指示是否显示辅助视图。store变量WorkoutListView自动继承,我们不需要显式传递它,但是我们需要对AddWorkoutView进行传递,因为它以表单的形式出现,而不是ContentView的子级

现在创建一个将从View继承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())
        }
    }
}

视图,使用容器List元素显示锻炼列表。onDelete函数用于删除锻炼并使用removeWorkout操作,该操作使用store提供调度功能执行。要在列表中显示锻炼,请使用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)
            )
        }
    }
}

这是一个相当大的控制器,与其他控制器一样,它包含store变量。它还包含变量nameText,distanceText,complexField和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()
        }
    }
}

该视图由两个按钮组成,这两个按钮负责对锻炼清单进行排序以及激活或停用锻炼清单的编辑模式。使用调度功能可以调用排序操作,我们可以通过store属性来调用它。

图片

结果


该应用程序已准备就绪,应该可以正常运行。让我们尝试编译并运行它。

发现


Redux和SwiftUI可以很好地协同工作。使用这些工具编写的代码易于理解,并且组织得很好。该解决方案的另一个优点是其出色的代码可测试性。但是,此解决方案并非没有缺点,其中之一是当应用程序的状态非常复杂时应用程序使用的大量内存,并且在某些特定情况下应用程序性能可能并不理想,因为SwiftUI中的所有视图在创建新状态时都会更新。这些缺点可能会对应用程序质量和用户交互产生重大影响,但是如果我们记住它们并以合理的方式准备状态,则可以轻松地最小化甚至避免负面影响。

您可以下载或查看项目的源代码。在这里

希望您喜欢这篇文章,并为自己学习了一些新知识,很快见。更进一步,它将变得更加有趣。

All Articles