
在参加WWDC 2019的国情咨文会议后,我决定详细研究SwiftUI。 我花了很多时间与他一起工作,现在我开始开发一个真正的应用程序,该应用程序对广泛的用户有用。
我称它为MovieSwiftUI-这是一个用于查找新电影和旧电影以及使用
TMDB API将它们收集到收藏集中的应用程序。 我一直热爱电影,甚至创建了一家在这方面工作很长时间的公司。 很难称赞公司很酷,但是申请-是的!
我们提醒您: 对于所有“ Habr”读者来说,使用“ Habr”促销代码注册任何Skillbox课程时均可享受10,000卢布的折扣。
Skillbox建议:在线教育课程“专业Java开发人员” 。
那么MovieSwiftUI会做什么?
- 与API交互-这几乎是所有现代应用程序所做的。
- 使用Codable在Swift模型中加载异步请求数据并解析JSON。
- 显示按需下载的图像并进行缓存。
- 此适用于iOS,iPadOS和macOS的应用程序为这些OS的用户提供了最佳的UX。
- 用户可以生成数据,创建自己的电影列表。 该应用程序保存并恢复用户数据。
- 使用Redux模式将视图,组件和模型清楚地分开。 数据流在这里是单向的。 它可以被完全缓存,还原和覆盖。
- 该应用程序使用SwiftUI,TabbedView,SegmentedControl,NavigationView,Form,Modal等基本组件。 它还提供自定义视图,手势,UI / UX。
实际上,动画很流畅,GIF有点抽搐处理该应用程序给了我很多经验,总的来说,这是一种积极的经验。 我能够编写一个功能完备的应用程序,9月份我将对其进行改进,并将其与iOS 13发行同时放入AppStore。
Redux,BindableObject和EnvironmentObject

我已经在Redux上工作了大约两年,所以我对此相当了解。 特别是,我在
React网站的前端以及开发本机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,但是协议实现负责管理它。 总而言之,这是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()) } }
当应用程序启动时,Store被实现为EnvironmentObject,然后可以使用@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滑动以进行删除。 因此,当用户触摸删除按钮时,他开始相应的操作并从列表中删除电影。
好吧,由于list属性是从BindableObject的状态派生而来的,并且是作为EnvironmentObject实现的,因此SwiftUI会更新列表,因为ForEach与计算的movie属性相关联。
这是MoviesState reducer的一部分:
func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState { var state = state switch action {
如上所述,当您提交操作并返回新状态时,将执行reducer。
我现在不会详细介绍-SwiftUI如何真正知道要显示的内容。 为了更深入地理解这一点,您应该
查看关于 SwiftUI中
数据流的WWDC会话 。 它还详细说明了为什么以及何时使用
State ,@Binding,ObjectBinding和EnvironmentObject。
Skillbox建议: