Last active
November 4, 2018 07:26
-
-
Save fumiyasac/4d784df5de2061fe17298531d12a64a4 to your computer and use it in GitHub Desktop.
ReduxとSwiftの組み合わせを利用したUIサンプル事例紹介 ref: https://qiita.com/fumiyasac@github/items/f25465a955afdcb795a2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Alamofire | |
import PromiseKit | |
import SwiftyJSON | |
class APIManagerForPickupMessage { | |
private let baseUrl = "APIのエンドポイントとなるURLを指定する" | |
// MARK: - Singleton Instance | |
static let shared = APIManagerForPickupMessage() | |
private init() {} | |
// MARK: - Functions | |
// PickupMessage一覧を取得する | |
func getPickupMessageList() -> Promise<JSON> { | |
// Alamofireの非同期通信をPromiseKitの処理でラッピングする | |
// 例. | |
// ----------- | |
// getNewsList(page: 0) | |
// .done { json in | |
// // 受け取ったJSONに関する処理をする | |
// print(json) | |
// } | |
// .catch { error in | |
// // エラーを受け取った際の処理をする | |
// print(error.localizedDescription) | |
// } | |
// ----------- | |
// 参考URL: | |
// https://medium.com/@guerrix/101-alamofire-promisekit-671436726ff6 | |
// ※ Swift4系では書き方が変わっているのでご注意を! | |
// https://stackoverflow.com/questions/48932536/swift4-error-cannot-convert-value-of-type-void-to-expected-argument-typ | |
return Promise { seal in | |
Alamofire.request(baseUrl, method: .get).validate().responseJSON { response in | |
switch response.result { | |
// 成功時の処理(表示に必要な部分だけを抜き出して返す) | |
case .success(let response): | |
let res = JSON(response) | |
let json = res["article"]["contents"] | |
seal.fulfill(json) | |
// 失敗時の処理(エラーの結果をそのまま返す) | |
case .failure(let error): | |
seal.reject(error) | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Storeの定義をあらかじめAppDelegate.swiftのクラス外に下記のように追記しておく | |
let appStore = Store(reducer: appReduce, state: AppState(), middleware: [ActionLoggingMiddleware]) | |
// PickupMessageReducerにてPickupMessageStateの更新が終わった後の処理の概要: | |
// ① PickupMessageStateの更新がStoreに反映された後は該当のViewController内でAppStateの更新を検知できるようする。 | |
// ② AppStateの更新を検知した際に必要なメソッドの処理内にPickupMessageStateの値に合わせた実装を行なっていく。 | |
// ※1 AppState()で行なっていること → PickupMessageStateをはじめ、このアプリで利用するStateを集約する | |
// ※2 AppReduceで行なっていること → PickupMessageReducerをはじめ、このアプリで利用するReducerを集約する | |
// ※3 MiddlewareではActionの実行前後で割り込ませて実行したい処理を記載する(今回は発行されたアクションのログ出力のみ) | |
@UIApplicationMain | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
・・・(省略)・・・ | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - MonthlyCalendarViewDelegate | |
extension MainViewController: MonthlyCalendarViewDelegate { | |
// 選択されたカレンダーの内容を反映させつつ、画面遷移を行う | |
func selectMonthlyCalendar(selectedDate: (selectedYear: Int, selectedMonth: Int, selectedDay: Int)) { | |
// 記事表示用の画面へ遷移する | |
let storyboard = UIStoryboard(name: "DailyMemo", bundle: nil) | |
let dailyMemoViewController = storyboard.instantiateViewController(withIdentifier: "DailyMemoViewController") as! DailyMemoViewController | |
dailyMemoViewController.setSelectedDate(selectedDate) | |
// カスタムトランジションのプロトコルを適用させて遷移する | |
let navigationController = UINavigationController(rootViewController: dailyMemoViewController) | |
navigationController.transitioningDelegate = self | |
self.present(navigationController, animated: true, completion: nil) | |
} | |
} | |
// MARK: - UIViewControllerTransitioningDelegate | |
extension MainViewController: UIViewControllerTransitioningDelegate { | |
// MEMO: 適用しているカスタムトランジションの詳細については、DailyMemoTransition.swiftを参照 | |
// 進む場合のアニメーションの設定を行う | |
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
// 現在の画面サイズを引き渡して画面が縮むトランジションにする | |
dailyMemoTransition.originalFrame = self.view.frame | |
dailyMemoTransition.presenting = true | |
return dailyMemoTransition | |
} | |
// 戻る場合のアニメーションの設定を行う | |
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
// 縮んだ状態から画面が戻るトランジションにする | |
dailyMemoTransition.presenting = false | |
return dailyMemoTransition | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - Protocol | |
// MEMO: ContainerViewを介したViewに関する処理はプロトコル経由で接続する | |
protocol MonthlyCalendarViewDelegate: NSObjectProtocol { | |
// 月別カレンダーのボタンタップ時にプロトコルを適用したViewController側で行うためのメソッド | |
func selectMonthlyCalendar(selectedDate: (selectedYear: Int, selectedMonth: Int, selectedDay: Int)) | |
} | |
class MonthlyCalendarViewController: UIViewController { | |
・・・(省略)・・・ | |
@objc private func calendarButtonTapped(button: UIButton) { | |
// Debug. | |
print("選択された日付:", "\(selectedYear!)年\(selectedMonth!)月\(button.tag)日") | |
// カレンダーで選択された日付を取得して、プロトコルを適用しているViewControllerに受け渡す | |
let targetSelectedDate: (selectedYear: Int, selectedMonth: Int, selectedDay: Int) = ( | |
selectedYear: selectedYear!, | |
selectedMonth: selectedMonth!, | |
selectedDay: button.tag | |
) | |
self.delegate?.selectMonthlyCalendar(selectedDate: targetSelectedDate) | |
} | |
・・・(省略)・・・ | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import ReSwift | |
extension PickupMessageState { | |
// ピックアップメッセージのstateを変更させるアクションをEnumで定義する | |
enum pickupMessageAction: ReSwift.Action { | |
// ピックアップメッセージの読み込み成功時にAPIより取得した値をセットするアクション | |
case setPickupMessage(pickupMessage: [PickupMessageEntity]) | |
// ピックアップメッセージエリアの表示状態の値をセットするアクション | |
case setIsPickupMessageAreaHidden(result: Bool) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import ReSwift | |
struct PickupMessageActionCreator {} | |
extension PickupMessageActionCreator { | |
// ピックアップメッセージ表示エリアの表示状態を反映する | |
static func shouldHidePickupMessageArea(result: Bool) { | |
// Storeにあるdispatchメソッドを実行してStateの更新要求を送る | |
appStore.dispatch( | |
// 該当するStateのextentionとして定義したActionを実行する | |
PickupMessageState.pickupMessageAction.setIsPickupMessageAreaHidden(result: result) | |
) | |
} | |
// ピックアップメッセージを取得する | |
static func fetchGourmetShopList() { | |
// ピックアップメッセージのAPIから全件情報を取得する | |
// MEMO: | |
// ① 各種APIのエンドポイントへのアクセスをする処理はAPIManagerクラスを別途用意している。 | |
// ② APIアクセスに関する処理ではSwiftyJSONとPromiseKitを併用している。 | |
// ③ Actionを発行する前にデータを取得や登録等の処理を実行して、その結果を一緒に引き渡す。 | |
APIManagerForPickupMessage.shared.getPickupMessageList() | |
.done { messageJSON in | |
// 成功時: 取得したJSONを解析したものを配列にしたものを引数に渡してアクションの実行 | |
let pickupMessageList = PickupMessage.getPickupMessagesBy(json: messageJSON) | |
appStore.dispatch(PickupMessageState.pickupMessageAction.setPickupMessage(pickupMessage: pickupMessageList)) | |
}.catch { error in | |
// 失敗時: 空配列を引数に渡してアクションの実行 | |
appStore.dispatch(PickupMessageState.pickupMessageAction.setPickupMessage(pickupMessage: [])) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import ReSwift | |
struct PickupMessageReducer {} | |
extension PickupMessageReducer { | |
static func reducer(action: ReSwift.Action, state: PickupMessageState?) -> PickupMessageState { | |
// ピックアップメッセージのstateを取得する(ない場合は初期状態とする) | |
var state = state ?? PickupMessageState() | |
// ピックアップメッセージのstateを変更させるアクションでない場合はstateの変更は許容しない | |
guard let action = action as? PickupMessageState.pickupMessageAction else { return state } | |
// ピックアップメッセージのstateを変更させるアクションに合わせてStateの更新を実行する | |
switch action { | |
case let .setPickupMessage(pickupMessage): | |
state.pickupMessageStateList = pickupMessage | |
case let .setIsPickupMessageAreaHidden(result): | |
state.isPickupMessageAreaHidden = result | |
} | |
// Debug. | |
AppLogger.printMessageForDebug("PickupMessageStateが更新されました。") | |
return state | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import ReSwift | |
// ピックアップメッセージの状態に関するstateの定義 | |
struct PickupMessageState: ReSwift.StateType { | |
// ピックアップメッセージの一覧を格納する配列(初期値: []) | |
var pickupMessageStateList: [PickupMessageEntity] = [] | |
// ピックアップメッセージの一覧エリアの表示フラグ(初期値: false) | |
var isPickupMessageAreaHidden: Bool = false | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class PickupMessageViewController: UIViewController { | |
・・・(省略)・・・ | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
// Stateが更新された際に通知を検知できるようにするリスナーを登録する | |
appStore.subscribe(self) | |
} | |
override func viewWillDisappear(_ animated: Bool) { | |
super.viewWillDisappear(animated) | |
// Stateが更新された際に通知を検知できるようにするリスナーを解除する | |
appStore.unsubscribe(self) | |
} | |
・・・(省略)・・・ | |
} | |
extension PickupMessageViewController: StoreSubscriber { | |
// Stateの更新が検知された際に実行される処理 | |
func newState(state: AppState) { | |
// TODO: PickupMessageStateの状態に合わせて配置したView要素を変更する処理を記載する | |
・・・(省略)・・・ | |
// Debug. | |
AppLogger.printStateForDebug(state.pickupMessageState, viewController: self) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ピックアップメッセージ画面に関するStateにおいてActionの発行から値反映までが実行されているかを確認するテスト | |
func testPickupMessageState() { | |
let appStore = Store(reducer: appReduce, state: AppState(), middleware: [ActionLoggingMiddleware]) | |
// Action発行前の値に関するテスト | |
let beforeIsPickupMessageAreaHidden = appStore.state.pickupMessageState.isPickupMessageAreaHidden | |
let beforePickupMessageStateList = appStore.state.pickupMessageState.pickupMessageStateList | |
XCTAssertEqual(false, beforeIsPickupMessageAreaHidden, "初期値はfalseである") | |
XCTAssertEqual(0, beforePickupMessageStateList.count, "初期値は空配列である") | |
// PickupMessageActionCreator内に定義した値を反映するアクションの実行 | |
appStore.dispatch( | |
PickupMessageState.pickupMessageAction.setIsPickupMessageAreaHidden(result: true) | |
) | |
// MEMO: スタブで用意した形式のJSONファイルを読み込んでAction実行時にJSONの値を反映させる | |
// ※ エンドポイントとほぼ同じレスポンスのJSONファイルを準備する | |
let filePath = Bundle.main.url(forResource: "PickupMessageStub", withExtension: "json")! | |
let messageJSON = JSON(try! Data(contentsOf: filePath)) | |
let pickupMessageList = PickupMessage.getPickupMessagesBy(json: messageJSON["article"]["contents"]) | |
appStore.dispatch( | |
PickupMessageState.pickupMessageAction.setPickupMessage(pickupMessage: pickupMessageList) | |
) | |
// Action発行後の値に関するテスト | |
let afterIsPickupMessageAreaHidden = appStore.state.pickupMessageState.isPickupMessageAreaHidden | |
let afterPickupMessageStateList = appStore.state.pickupMessageState.pickupMessageStateList | |
// (1) 表示フラグの変更 | |
XCTAssertEqual(true, afterIsPickupMessageAreaHidden, "setIsPickupMessageAreaHiddenの引数と同じ値となる") | |
// (2) JSONからの表示用データ生成 | |
XCTAssertEqual(18, afterPickupMessageStateList.count, "pickupMessageStub.json内の表示用データの個数と同じ値となる") | |
// (3) 表示用データの構成要素 | |
let firstData: PickupMessageEntity = afterPickupMessageStateList.first! | |
XCTAssertEqual("1", firstData.id, "pickupMessageStub.jsonのデータ部分の1番目に相当するID文字列と同じ値となる") | |
XCTAssertEqual("旬の魚が安く手に入る「いきいき魚市」", firstData.title, "pickupMessageStub.jsonのデータ部分の1番目に相当するタイトル名と同じ値となる") | |
XCTAssertEqual("ショッピング・お買い物", firstData.category, "pickupMessageStub.jsonのデータ部分の1番目に相当するカテゴリ名と同じ値となる") | |
XCTAssertEqual("https://kanazawa-photos.s3-ap-northeast-1.amazonaws.com/articles/images/1/large.jpg", firstData.imageUrl, "pickupMessageStub.jsonのデータ部分の1番目に相当する画像ファイルURLの文字列と同じ値となる") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment