SwiftUIでのアプリケーション開発。 パート 1: データフローと Redux

SwiftUIでのアプリケーション開発。 パート 1: データフローと Redux

WWDC 2019 の一般教書セッションに出席した後、私は SwiftUI を深く掘り下げることにしました。 私はこれに多くの時間を費やし、現在、幅広いユーザーにとって役立つ実際のアプリケーションの開発を開始しています。

私はそれを MovieSwiftUI と名付けました。これは新しい映画と古い映画を検索し、それらをコレクションに収集するためのアプリです。 TMDB API。 私は昔から映画が大好きで、かなり昔のことですが、この分野の会社を設立したこともありました。 会社は決してクールとは言えませんでしたが、アプリケーションは素晴らしかったです。

リマインダー: 「Habr」のすべての読者が対象 - 「Habr」プロモーション コードを使用してスキルボックス コースに登録すると 10 ルーブルの割引。

スキルボックスは次のことを推奨します。 教育オンラインコース 「職業 Java 開発者」.

では、MovieSwiftUI では何ができるのでしょうか?

  • API と対話します - ほとんどすべての最新のアプリケーションがこれを行います。
  • リクエストで非同期データをロードし、JSON を解析して Swift モデルに組み込みます。 コード化可能.
  • リクエストに応じてロードされた画像を表示し、キャッシュします。
  • iOS、iPadOS、macOS 用のこのアプリは、これらの OS のユーザーに最高の UX を提供します。
  • ユーザーはデータを生成し、独自の映画リストを作成できます。 アプリケーションはユーザー データを保存および復元します。
  • ビュー、コンポーネント、モデルは Redux パターンを使用して明確に分離されます。 ここでのデータ フローは一方向です。 完全にキャッシュ、復元、上書きすることができます。
  • アプリケーションは、SwiftUI、TabbedView、SegmentedControl、NavigationView、Form、Modal などの基本コンポーネントを使用します。 カスタムビュー、ジェスチャー、UI/UX も提供します。

SwiftUIでのアプリケーション開発。 パート 1: データフローと Redux
実際、アニメーションはスムーズですが、GIF は少しぎくしゃくしています。

アプリに取り組むことで多くの経験が得られ、全体的には前向きな経験でした。 完全に機能するアプリケーションを作成できたので、13 月にそれを改良して、iOS XNUMX のリリースと同時に AppStore で公開する予定です。

Redux、BindableObject、EnvironmentObject

SwiftUIでのアプリケーション開発。 パート 1: データフローと Redux

私は Redux を約 XNUMX 年間扱っているので、比較的よく理解しています。 特に、フロントエンドで使用します。 反応する Web サイトだけでなく、ネイティブ iOS (Swift) および Android (Kotlin) アプリケーションの開発にも使用できます。

私は、SwiftUI アプリケーションを構築するためのデータ フロー アーキテクチャとして Redux を選択したことを後悔したことはありません。 UIKit アプリで Redux を使用するときに最も困難な部分は、ストアと連携してデータを取得および取得し、それをビュー/コンポーネントにマッピングすることです。 これを行うには、一種のコネクタ ライブラリを (ReSwift と ReKotlin を使用して) 作成する必要がありました。 うまく動作しますが、コードがかなり多くなります。 残念ながら、これは(まだ)オープンソースではありません。

朗報です! Redux を使用する予定がある場合、SwiftUI で心配する必要があるのは、ストア、ステート、およびリデューサーだけです。 @EnvironmentObject のおかげで、ストアとのやり取りは SwiftUI によって完全に処理されます。 したがって、ストアは BindableObject から始まります。

シンプルなSwiftパッケージを作成しました。 SwiftUIFluxRedux の基本的な使用方法を説明します。 私の場合、それは 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())
    }
}

ストアはアプリケーションの起動時にEnvironmentObjectとして挿入され、@EnvironmentObjectを使用して任意のビューでアクセスできるようになります。 派生プロパティはアプリケーションの状態から迅速に取得または計算されるため、パフォーマンスの低下はありません。

上記のコードは、映画のポスターが変更されると画像を変更します。

そして、これは実際にはたった XNUMX 行で行われ、どのビューが状態に接続されているかを使用します。 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 状態から派生し、EnvironmentObject として挿入されるため、ForEach が映画の計算済みプロパティに関連付けられているため、SwiftUI はリストを更新します。

以下は 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
}

前述したように、リデューサーはアクションをディスパッチして新しい状態を返すときに実行されます。

SwiftUI が実際に何を表示するかをどのように認識するかについては、まだ詳しく説明しません。 これをより深く理解するには価値があります データ フローに関する WWDC セッションを表示する SwiftUIで。 使用する理由とタイミングについても詳しく説明します 都道府県、@Binding、ObjectBinding、EnvironmentObject。

スキルボックスは次のことを推奨します。

出所: habr.com

コメントを追加します