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。

技能箱推薦:

來源: www.habr.com

添加評論