Skip to content

Instantly share code, notes, and snippets.

@fumiyasac
Last active November 4, 2018 07:26
Show Gist options
  • Save fumiyasac/4d784df5de2061fe17298531d12a64a4 to your computer and use it in GitHub Desktop.
Save fumiyasac/4d784df5de2061fe17298531d12a64a4 to your computer and use it in GitHub Desktop.
ReduxとSwiftの組み合わせを利用したUIサンプル事例紹介 ref: https://qiita.com/fumiyasac@github/items/f25465a955afdcb795a2
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)
}
}
}
}
}
// 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 {
・・・(省略)・・・
}
// 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
}
}
// 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)
}
・・・(省略)・・・
}
import Foundation
import ReSwift
extension PickupMessageState {
// ピックアップメッセージのstateを変更させるアクションをEnumで定義する
enum pickupMessageAction: ReSwift.Action {
// ピックアップメッセージの読み込み成功時にAPIより取得した値をセットするアクション
case setPickupMessage(pickupMessage: [PickupMessageEntity])
// ピックアップメッセージエリアの表示状態の値をセットするアクション
case setIsPickupMessageAreaHidden(result: Bool)
}
}
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: []))
}
}
}
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
}
}
import Foundation
import ReSwift
// ピックアップメッセージの状態に関するstateの定義
struct PickupMessageState: ReSwift.StateType {
// ピックアップメッセージの一覧を格納する配列(初期値: [])
var pickupMessageStateList: [PickupMessageEntity] = []
// ピックアップメッセージの一覧エリアの表示フラグ(初期値: false)
var isPickupMessageAreaHidden: Bool = false
}
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)
}
}
// ピックアップメッセージ画面に関する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