RxSwift研究読本で、RxSwiftのお勉強
RxSwiftの概要
RxSwiftとは何か
iOS開発のイベント処理
RxSwiftは複雑になりがちなイベント処理をデータの流れ(ストリーム)として統一的に扱うReactiveX(Reactive Extensions: Rx)をベースとしたライブラリです。 イベント発生時に流れてくるストリームに反応(リアクション)して処理を行うことを実現します。
Rxとは、オブザーバーパターン、イテレータパターン、関数型プログラミングのアイデアを組み合わせたもの
キーワード
- ストリーム
- データがイベントとして連なった流れ
- Observable
- RxSwiftにおけるストリームを生産するもの。クラスObservable
で提供される
- RxSwiftにおけるストリームを生産するもの。クラスObservable
- オペレータ
- ストリームに対して処理を行うメソッド。mapやfilterなど
- ストリームの購読
- ストリームから伝播されてくるイベントを順次処理する仕組み
RxSwiftによるMVVMパターン
- View(ViewController)
- UIロジックを担当
- Viewの生成、表示非表示、タッチイベントの設定など
- ViewModel
- プレゼンテーションロジックを担当
- ビジネスロジックの結果をUIに表示するための処理
- 具体的なUI表示への関心を分離している
- Model
- ビジネスロジックを担当
- MVVMではViewとViewModel以外の全てがModelであると言える
MVVMとはViewとViewModelの分離とデータバインディングのこと
→Modelを細分化するようなアーキテクチャと両立するもの。
UML図で理解するRxSwift
Subject
- Observable, Observer両方の機能を有している
- Observableクラスを継承し、ObserverTypeプロトコルを採用している
import RxSwift let subject = PublishSubject<String>() subject.subscribe(onNext: { print("onNext: \($0)") }) subject.onNext("A") subject.onNext("B") subject.onNext("C") subject.onCompleted() subject.onNext("D") //--output-- //onNext: A //onNext: B //onNext: C
- Subjectは購読される
- Subjectのインスタンスはイベントを任意のタイミングで発火できる
ControlProperty
- RxCocoaで定義
- Observable, Observer両方の機能を有している
- UIコンポーネントからのイベントをストリームとして取得するための型
RxExampleによるRxとMVVMの解説
- ViewController
- ViewModel
disposeBagとは
- まとめてObservableを処分するための仕組み
- disposeBagにObservableを保持させ、そのdisposeBagを破棄することでObservableをまとめて破棄できる!
- ViewControllerが破棄されるときに、そのプロパティも自動で破棄されるため、disposeBagの仕組みが働くようになっている
IBOutletからObservableの作成
let observable: Observable<String> = textField.rx.text.orEmpty.asObservable()
ViewModelの出力をViewにバインド
subscribe
ViewController
viewModel.signupEnable.subscribe( onNext: { [weak self] valid in self?.signupOutlet.isEnable = valid }).disposed(by: disposeBag)
ViewControllerでviewModelが持つObservableを購読し、イベントを発火している。
bind(to:)
ViewController
viewModel.validatedUsername .bind(to: usernameValidationOutlet.rx.validationResult) .disposed(by: disposeBag)
viewModelが持つObservableを購読し、RxCocoaのControlPropertyにストリームを流している?
雑メモ
Observable.create
https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Observables/Create.swift
引数にsubscribe((AnyObserver
Fluxアーキテクチャのお勉強
アプリ開発するんだから、実装より先にアーキテクチャの部分を決めて行かないといけないよね。
# 今回作るアプリ
今回はレシピ管理アプリみたいなやつを作ろうと思う。
- レシピ一覧
- iOSでいうところのTableView。セルをタップすると詳細画面に遷移する
- レシピ作成
- データはとりあえずローカルに保存する。でも将来はサーバーに持ちたい。
# ReactNativeのアーキテクチャってどんなん?
Reactと同じようにReduxを用いたFluxパターンが王道っぽい。
まあ、勉強だと思ってReduxを採用しましょう。
ちなみに筆者はReactも初心者なのでReduxをちゃんと理解していません。
# Fluxってなんなん?
https://github.com/facebook/flux/tree/master/examples/flux-concepts
以下、意訳。
> Fluxはデータフローを管理するためのアーキテクチャパターンです。一番大事なのは、Fluxはデータの流れが単方向であることです。
ふむふむ
> Fluxには4つの部品があります。
## Dispatcher
> DispatcherはActionを受け取り、そのアクションをDispatcherに登録したStoreへ送ります。全てのStoreが全てのActionを受け取ります。アプリケーションの中でDispatcherはシングルトンであるべきです。
ふむふむ。Actionとは?Storeとは?
## Store
> Storeはアプリケーションのデータを保持しておくものです。StoreはActionを受け取るためにDispatcherを登録します。StoreのデータはActionを受け取った時にしか変更されません。publicでいいのはゲッターだけで、publicなセッターをStoreに持つべきではありません。StoreはなんのActionを受け取るか決めます。Storeのデータが変更されるといつも、changeイベントが発火します。アプリケーション内にStoreはたくさんあります。
ふむふむ。
どこか(Viewとか?)でActionがDispatcherに送られて、DispatcherがそのActionを然るべきStoreに振り分けるみたいなイメージかな?
## Actions
>アクションはアプリケーション内部のAPI定義です。どんな種類のインタラクトが発生したかを捕捉します。Actionはtypeフィールドといくつかのデータを持った単純なオブジェクトです。Actionはセマンティックで、発生したアクションを説明するものであるべきです。Actionの実装の詳細を説明するものではありません。「ユーザーIDを消す」「ユーザーデータを消す」みたいなものではなく、「ユーザーを消す」というようなものを使いましょう。全てのStoreは同じAction、例えば「ユーザーを消す」Actionをハンドリングして、データを消すのか、クレデンシャルを更新するのかなどを判別します。
よくわからんけど、Storeでなんらかの操作を行うそのトリガーとその引数、みたいなことであってるかな。
## Views
>StoreからのデータはViewに表示されます。 Viewはあなたが望むどんなフレームワークも使用できます(ここでのほとんどの例では、Reactを使用します)。 ViewがStoreからのデータを使用する場合、そのStoreからの変更イベントをサブスクライブする必要もあります。 その後、Storeが変更を発行すると、Viewは新しいデータを取得して再レンダリングします。 コンポーネントがStoreを使用し、それをサブスクライブしない場合、微妙なバグが存在する可能性があります。 Actionは通常、ユーザーがアプリケーションのインターフェイスの一部を操作するときにViewから送出されます。
ViewがStoreをサブスクライブ(購読/監視)する。Storeのデータが更新される。それを検知して新しいデータで再レンダリングする。なるほど。
## Example
>
- ViewはTodoStoreをサブスクライブ(購読/監視)する。
- todoを追加するActionを定義する。
```
{
type: 'add-todo',
title: 'タイトル',
}
```
>
- ユーザーが新しいTodoのタイトルを入力して完了すると、ViewはDispatcherにタスクのタイトルを含む「add-todo」アクションををディスパッチするよう指示する。
- すべてのストアがディスパッチされたアクションを受け取る。
- 自分に関係のあるActionだったら、そのActionを処理する。
- この場合TodoStoreはアクションを処理し、別のTodoを内部データ構造に追加してから、changeイベントを発行する。
- Viewはchangeイベントをリッスンしています。 イベントを取得し、TodoStoreから新しいデータを取得して、Todoのリストを再レンダリングする。
ぐるっと回って帰ってくる、みたいなことね!!つまり単方向のデータフローだ!!!
この中でDispatcherは全てのActionを受け取って全てのストアにそれをぶん投げる、Actionの問屋みたいなことね!
(一元管理したいのでシングルトンであることが望ましい、と)
## Data Flow
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248316/29c139b1-c955-3888-e6ea-4b5bc4ed6879.png)
よくみる画像を拝借。うんうん。こうやってみると、わかりやすい。
# なんでReact系はFluxアーキテクチャがいいんだろう?
モバイルアプリとかではあまり聞かないよね。サクッと調べたらVue.jsはMVVMが有名っぽい?うーん。詳しい人がいたら教えて欲しい。
## Reduxってなんだ?
さて。Reduxってなんだ?
https://redux.js.org/basics/basic-tutorial
公式のチュートリアルを追ってみよう
> ReducerとかミドルウェアとかStoreへの派手な話に騙されないで。Reduxってマジで単純なんだ
信じよう。
## Actions
> Actionは、アプリケーションからStoreにデータを送信する情報のペイロードです。 Storeの唯一の情報源です。 それらは、store.dispatch()を使用してStoreに送信します。
ここの役割というか定義はFluxと同じっぽい。
```
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
```
## Reducers
これはなんだ?
> Reducerは、Storeに送信されたActionに応じてアプリケーションの状態がどのように変化するかを指定します。 Actionは何が起こったかを説明するだけで、アプリケーションの状態がどのように変化するかを説明しないことに注意してください。
つまり?
https://redux.js.org/basics/reducers
全然わかんないじゃん!?
誰か助けて。。。ということで
https://qiita.com/tkow/items/9da7062f9bfa99e848c3
つまり、ReducerがActionを受け取って、そのタイプに応じて適切な関数によって値を変化させる、ということっぽい。
めちゃめちゃざっくり。
Redux 入門(サンプルコードを読む)
React/Redux初心者の私が、Reduxのサンプルコードを見てみる回です。 https://github.com/reduxjs/redux/tree/master/examples
対象としては「用語はわかるけどどうやって使うのかいまいちわからん」な人です。 つまり私です。
ディレクトリ構成
見てみる。
手元でビルドできない。
import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default App
まあ、Todo作成する部分と、表示する部分と、フッターがあるみたい。 表示する部分から見てみよう
import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' import { VisibilityFilters } from '../actions' const getVisibleTodos = (todos, filter) => { switch (filter) { case VisibilityFilters.SHOW_ALL: return todos case VisibilityFilters.SHOW_COMPLETED: return todos.filter(t => t.completed) case VisibilityFilters.SHOW_ACTIVE: return todos.filter(t => !t.completed) default: throw new Error('Unknown filter: ' + filter) } } const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter) }) const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) }) export default connect( mapStateToProps, mapDispatchToProps )(TodoList)
おっ、Viewを持っていない? Viewの本体はこっちみたいだ。
import React from 'react' import PropTypes from 'prop-types' import Todo from './Todo' const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> )} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired).isRequired, toggleTodo: PropTypes.func.isRequired } export default TodoList
connectってなんだろう?
export default connect( mapStateToProps, mapDispatchToProps )(TodoList)
なんだろう。 Reduxライブラリからimportしている。
https://react-redux.js.org/api/connect
The connect() function connects a React component to a Redux store.
おー。つまりVisibleTodoList.jsがStoreなんだね。 StoreがViewに依存する状態。
じゃあこの引数で渡しているのは何?
mapStateToProps, mapDispatchToPropsってなんだろう?
React ComponentとStoreをconnectする(きっと、StoreがComponentに対してイベントを送れる状態)時に引数で渡すこのふたつ。
const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter) }) const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) })
stateはまあわかる。
const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter) })
const getVisibleTodos = (todos, filter) => { switch (filter) { case VisibilityFilters.SHOW_ALL: return todos case VisibilityFilters.SHOW_COMPLETED: return todos.filter(t => t.completed) case VisibilityFilters.SHOW_ACTIVE: return todos.filter(t => !t.completed) default: throw new Error('Unknown filter: ' + filter) } }
リストとTypeを渡すとフィルターしたTodoのリストを返すような関数を渡している。 それがPropsとしてViewに渡り、それをmapして描画している。 じゃあgetVisibleTodoListに引数を渡すのは誰なんだろう?
とりあえずその下、dispatchも見てみよう。
const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) })
toggleTodoというのはActionだ。
export const toggleTodo = id => ({ type: 'TOGGLE_TODO', id })
StoreにtoffleTodoというActionをディスパッチしている。で、それもViewにconnectされる。 Viewの中で
const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> )} </ul> )
というのがあるから、セルをタップするとそのTodoの状態が変わるのかな。 このActionがどうやってViewに伝わるのか。Reducerにわたるはずやけど。
と思っていたら、鍵はこれだった
import React from 'react' import { render } from 'react-dom' import { createStore } from 'redux' import { Provider } from 'react-redux' import App from './components/App' import rootReducer from './reducers' const store = createStore(rootReducer) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
いっちばん上でrootReducerからStoreを作って、下位コンポーネント全てにぶん投げている。 つまり単一のストアをアプリケーション全体で共有している状態。 そしてこのrootReducerというのは
import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos, visibilityFilter })
こいつを指している。 これはさらに
const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text, completed: false } ] case 'TOGGLE_TODO': return state.map(todo => (todo.id === action.id) ? {...todo, completed: !todo.completed} : todo ) default: return state } } export default todos
こいつらを表している。
rootReducerってなんだ?
React.js - rootReducerってなんですか?|teratail なるほど。。
createStoreとは?
reducerからStoreを作る。 Storeはreduser毎にstateを作成する?
Storeとは?
わからんち
Providerとは?
これってもしかして、その下位コンポーネントがStoreの役割を持てる(connectとかdispatcherを自分の関数のように扱える)ことと関係がある?ありそう。 Providerしたらその下位コンポーネントは渡したStoreの役割を持てるっぽい。
一旦まとめ
流れとしては、 1. ルートのindex.jsが呼ばれる 2. rootReducer(reducersディレクトリ 直下のcombineReducer)を使ってStoreを作成する 3. storeが下位コンポーネントにぶん投げられる?(Provider) 4. VisibleTodoList.jsでViewとStoreをconnectする(その時にストアが更新されるたび呼び出される関数とstateを書き換えるdispatchを定義する) 5. Viewでdispatchを呼び出す 6. dispatchがアクションを発火させてreducerがstoreのstateを更新する 7. storeの更新を検知してViewのストアが更新されるたび呼び出される関数が走る 8. それに応じてViewが再レンダリングされる
参考リンク
ReactNativeの話
Flutterのほうがイマドキっぽいのかもしれないが、今後伸びるのはReactNativeな気がする
なぜReactNative?
- ネイティブアプリで新規開発するモチベーションってどんどん薄れていきそう。
- クロスプラットフォーム系ライブラリが発展すればするほど、「ネイティブでないといけない理由」が無くなってくる
- FlutterやXmarinもあるけど、Web(React)エンジニアがとっつきやすいというのは大きいメリット。
- 実際、Webのフロント部隊がそのままReactNativeでスマホアプリを開発しているケースもある https://www.atmarkit.co.jp/ait/articles/2003/31/news004.html
ReactNativeって何?
ネイティブで動くやつ、って意味でNativeが付くんだろう。 当たり前だけどReactとは言えReactではなく、React風に書けるJavaScriptライブラリ。ビルドされると、ReactNativeで書かれたコンポーネントは各プラットフォーム毎にUIクラスを生成する。(ViewControllerとActivity、とか。) 昔のクロスプラットフォームはHTML5で書いてWebViewで。。。みたいな、めちゃしょぼいやつだったけど、ReactNativeとか、あんまり詳しく無いけどXamarinとか、Flutterとか、実際にネイティブで動くクロスプラットフォームのライブラリが生まれてから、リッチなインタラクションを実現できるようになっていろんなプロダクトで採用されていると思う。
ReactNativeはどんなアプリで使われているの?
React自体がFacebook製というのもあって、FacebookとInstagramはReactNativeだったはず。AirbnbはReactNativeだったけど今はネイティブになったらしい。 SNS系はそんなにUIに凝らない(アニメーションとか)けど、AirbnbとかはLottieみたいなアニメーションライブラリ出してたり、いろいろ力を入れている印象なので、ReactNativeでは物足りなくなったのかもしれない。ちなみにUber/UberEatsもReactNativeらしい。
ReactNativeをやってみる
ReactNativeの環境構築
https://reactnative.dev/docs/environment-setup 公式が、「Expo-CLIを使え!」と言ってくる。 チュートリアルの通りにコマンド打ち込んで、Expoインストール→プロジェクト作成しましょう。 (TypeScriptでやりたいので、そうした)
Expoって何?
ReactNativeのライブラリ。なんかいろいろ簡単にやらせてくれるらしい。 その分制約がある(ネイティブコードを弄れない、とか?) https://trilingual-engineer.com/expo-vs-react-native
アプリを起動してみる
プロジェクト直下のディレクトリ で
expo start
を実行すると、
こんな画面に飛ばされるので、起動したいデバイス(シミュレーター とか実機とか)を選択する。 そしたら、そのデバイスにExpoアプリがインストールされて、その上でさっき作成したReactNativeのアプリが動く。 これはホットリロードなので、デバッグする時は保存すると勝手に反映される。はず。