SwiftUI 上的应用程序开发。 第 1 部分:数据流和 Redux

SwiftUI 上的应用程序开发。 第 1 部分:数据流和 Redux

在参加 WWDC 2019 的国情咨文会议后,我决定深入研究 SwiftUI。 我花了很多时间来使用它,现在已经开始开发一个对广泛用户有用的真实应用程序。

我将其命名为 MovieSwiftUI - 这是一个用于搜索新旧电影以及使用以下命令将它们收集到集合中的应用程序 TMDB API。 我一直很喜欢电影,甚至创建了一家从事这个领域的公司,尽管是很久以前的事了。 这家公司很难说很酷,但它的应用程序却很酷!

我们提醒: 对于“Habr”的所有读者 - 使用“Habr”促销代码注册任何 Skillbox 课程可享受 10 卢布的折扣。

技能箱推荐: 教育在线课程 《职业Java开发人员》.

那么 MovieSwiftUI 能做什么呢?

  • 与 API 交互——几乎所有现代应用程序都会这样做。
  • 根据请求加载异步数据,并使用以下命令将 JSON 解析为 Swift 模型 可编码.
  • 显示根据请求加载的图像并缓存它们。
  • 这款适用于 iOS、iPadOS 和 macOS 的应用程序为这些操作系统的用户提供了最佳的用户体验。
  • 用户可以生成数据并创建自己的电影列表。 该应用程序保存和恢复用户数据。
  • 使用 Redux 模式将视图、组件和模型清晰地分开。 这里的数据流是单向的。 它可以被完全缓存、恢复和覆盖。
  • 该应用程序使用了SwiftUI、TabbedView、SegmentedControl、NavigationView、Form、Modal等基本组件。 它还提供自定义视图、手势、UI/UX。

SwiftUI 上的应用程序开发。 第 1 部分:数据流和 Redux
其实动画很流畅,GIF结果有点生涩

开发该应用程序给了我很多经验,总的来说,这是一次积极的体验。 我能够编写一个功能齐全的应用程序,13月份我将对其进行改进并将其发布在AppStore中,与iOS XNUMX的发布同时进行。

Redux、BindableObject 和 EnvironmentObject

SwiftUI 上的应用程序开发。 第 1 部分:数据流和 Redux

我使用 Redux 已经有大约两年了,所以我对它比较熟悉。 特别是,我在前端使用它 应对 网站,以及开发原生 iOS (Swift) 和 Android (Kotlin) 应用程序。

我从未后悔选择 Redux 作为构建 SwiftUI 应用程序的数据流架构。 在 UIKit 应用程序中使用 Redux 时,最具挑战性的部分是使用存储、获取和检索数据并将其映射到视图/组件。 为此,我必须创建一种连接器库(使用 ReSwift 和 ReKotlin)。 效果很好,但是代码量很大。 不幸的是,它(还)不是开源的。

好消息! 如果您打算使用 Redux,那么使用 SwiftUI 唯一需要担心的是存储、状态和化简器。 借助 @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,当它的值发生变化时,它会使用 PassthroughSubject 提供的 willChange 属性通知 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 在任何视图中访问。 由于可以从应用程序状态快速检索或计算派生属性,因此不会造成性能损失。

如果电影海报发生变化,上面的代码也会更改图像。

这实际上只需一行即可完成,借助该行将视图连接到状态。 如果您曾在 iOS 上使用过 ReSwift,甚至 联接 通过 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!]))
            }
        }
    }
}

在上面的代码中,我对每个 IP 使用 SwiftUI 中的 .onDelete 操作。 这允许列表中的行显示正常的 iOS 滑动删除。 因此,当用户触摸删除按钮时,它会触发相应的操作并将电影从列表中删除。

好吧,由于列表属性派生自 BindableObject 状态并作为环境对象注入,因此 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
}

当您分派一个操作并返回一个新状态时,reducer 就会被执行,如上所述。

我不会详细介绍 - SwiftUI 实际上如何知道要显示什么。 为了更深入地理解这一点,值得 查看有关数据流的 WWDC 会话 在 SwiftUI 中。 它还详细解释了为什么以及何时使用 州/领地、@Binding、ObjectBinding 和 EnvironmentObject。

技能箱推荐:

来源: habr.com

添加评论