iOSDC 2018 へ参加しました!

最近は、SwiftよりJavaScriptを書いている Hondaです。
8/30~9/2にわたって、早稲田大学 理工学部西早稲田キャンパスで行われた iOSDC 2018 に一般参加してきました。

当日の様子

当日の画像は、iOSDC スタッフブログ で公開されていました。

cv.三石琴乃 さんのスポンサー紹介から熱い本編三日間でした。

特に参考になったセッション

CodePushとReact Nativeで緊急OTAリリース!了解!


弊社でもReact Nativeを使って、クロスプラットフォームでアプリを開発をしています。Web開発とは違いiOSには、Appleの審査が必要です。そのため、緊急リリースが必要な場面であっても、どうしても待ち時間が発生してしまいます。

CodePushは、マイクロソフトが開発しているSaaSです。React Nativeで動作するJavaScriptのコード本体(jsBundleの部分)を、リモートから更新することができるサービスです。機能追加等はAppleの規約上で禁止されていますが、緊急な不具合が発生した局面などを想定して、導入の検討を進めていこうと考えています。

海外展開を目指すアプリで実装したセキュアで高速な画像配信の話


海外展開において、レイテンシーは切っても切れない要素です。New Relic Syntheticsを使った、世界各地に設定されている場所からの通信をシミュレートした知見は、大変参考になりました。

弊社でも解析情報を元にアプリの高速化し、よいUXを体験できるよう活用して行きたいです。

iOSエンジニアが知るべきProgressive Web Apps開発のエッセンス


Safariの新バージョンにSearvice Workerが対応したことで、一段と盛り上がっているPWAについてです。個人的に注目していることもあり、気になるセッションでした。

ただ、PWA on iOSの実情が以下であることは、予想外の内容で驚きでした。

  • Service Workerが使えるようになった。
  • Web Clipがweb Manifestに対応した。
  • 実はこれだけ

又、Android側に比べて実装が不完全であるため、別物であるということがあるとのことです。
しかし、進化の早いWebの環境から知識を取り入れられる流れも多くあるので、キャッチアップして行きたいと考えています。

iPhoneが数秒おきにクラッシュするんだけど


LocalNotificationが原因で、OS自体がクラッシュする致命的な問題に対応した知見の発表でした。
原因のログがない状態でも、最速で再現コードを作成し、修正版を作成します。対応した内容については、実際に体験しないとわからない内容だったため、参考になりました。

以下の内容は、OS起因の不具合を特定するにはかなり有効とのことです。

  • twitterで共有する。
  • 再現するデモコードを作成する。
  • 英語で騒ぐ
  • 修正版をつくり特急審査をする

周りと不具合を共有することで、一緒に解決していこうというアプローチはとても参考になります。

Good Practices for a Robust View Layout


レイアウトの崩れをおこしやすい、やってしまいがちなアンチパターンを元に実践的な内容で大変参考になりました。

弊社のiOSアプリでも積極的にUIStackViewを使用して開発をしています。テストの指針については、まだまだブラッシュアップの段階にある為、参考にさせて頂きます。

==============================

こんにちは、Sasatenです。
Hondaと同じく、iOSDCに参加して来ました。印象に残ったセッションをご紹介させていただきます。

設計時空のリファクタリング

「リファクタリングに銀の弾丸はない」という言葉が、とても印象に残りました。
私はリファクタリングの難しいところは、それによって保守性やメンテナンスは向上するものの、効果がすぐに現れるわけではないので、経営層の理解を得られづらい点にあると考えています。

そこで、大規模なリファクタリングをするわけではなく「機能開発:リファクタリング」を「7:3」のバランスで実践するという手法が紹介されています。小さい、コツコツとした改善を繰り返すのです。

その過程で、少しずつ単体テストも導入していきます。手動によるテストも組み合わせることで、デグレをおこさずリファクタリングを繰り返していくのです。

iOS×React Nativeのハイブリッド開発現場から伝えたい事

dex.fmでもお馴染みの@hotchemiさんの講演です。弊社でも「React Native」を使っているのですが、似たような感想を持っているため、終始うなずいてました。

私は、React Nativeでアプリを開発するポイントは、いかにJavaScriptのレベルで完結させ、ネイティブ連携を減らすことが出来るかだと考えています。

ハイブリッドアプリ開発が、アプリ開発のスピードを加速させることは疑いようがありません。ただ、ネイティブ連携を増やすと、途端に保守やメンテナンスが大変になっています。

弊社でも「React Native」はまだ採用したばかりですので、今後も情報はキャッチアップしていきたいです。

State of the Union 〜 2018年のアプリ開発事情

「流行りのアーキテクチャを無理に取り入れる必要はない。しかし、そのアーキテクチャが流行っている背景には、必ず求められている理由がある」
この言葉が、とても印象に残りました。

現在のアプリ開発事情を俯瞰して冷静に捉えており、今後の技術選定や、方向性の決定にも役に立つセッションっだったと思います。
スライドを見るだけでも、今後のアプリ開発に必要な技術をキャッチアップすることができます!オススメです。

全部iOSにしゃべらせちゃえ!

本当に採択されました。会場では、ずっと爆笑の嵐です。
LTではビールが振る舞われるのですが、とにかくお酒のススムセッションです。

あまりに面白すぎて、それまでに聞いたセッションの内容が全て吹っ飛びそうな勢いの名セッションです。

まとめ

発表者の方、スタッフの方、参加者の皆様、お疲れ様でした。おいしいランチ、快適なWifi環境で、iOSづけの3.5日間でした。参加者のほとんどがiOSに関わる開発者で、同じ課題を共有する空間はモチベーションが上がります。興味がある方は、来年参加してみるのはいかがでしょうか。

共同執筆: ササテン

iOS アーキテクチャ Redux + ApplicationCoordinator

はじめまして。今回がエンジニアブログ初投稿で、カタリストシステム最年少のホンダです!
今回は、新規プロダクト開発時に悩み抜いて選定したアーキテクチャについて書きます。
今までは、iOSの基本的な設計思想であるApple’s MVCを使って、iOSアプリを開発していました。

しかし、開発が進むにつれて以下のような問題が発生しました。

  • ViewControllerが肥大化する
  • 画面遷移が複雑化する
  • データの引き渡しがやりづらい

上記の問題を解決するために、今回新たに開発する新規プロダクトでは、Redux(ReSwift) + ApplicationCoordinator を使った、データの同期と遷移の共通化を実装しています。
本記事では、まず概要を説明し、記事の後半で実際のコードをご紹介致します。

Reduxとは

ReSwiftの根本には、Reduxの思想があります。
Reduxは、UIのstate(状態)を管理をするためのフレームワークです。stateでデータフロー管理する実装にFluxが提案されていますが、ReduxはFluxの概念を拡張してより扱いやすく設計されています。
Reduxのデータフローは以下になります。

  • Action 「何をする」という情報を持ったオブジェクトです。
  • Store アプリケーションの状態(state)を保持している場所です。最大で1つです。
  • State アプリケーションでの状態を表します。
  • Reducer ActionとStateを使ってStore内のStateを更新します。

Reduxの3原則

Reduxの基本設計は以下の3つの原則に基づいて設計されています。
上記のデータフローはこの原則に則っていることがよく分かると思います。

  1. Single source of truth
    アプリケーション内でStoreは1つのみとし、Stateは単独のオブジェクトとしてStoreに保持される。
  2. State is read-only
    Stateを直接変更することはできず、actionをStoreへdispatchすることでしかStateは変更できない。
  3. Mutations are written as pure functions
    Stateを変更する関数(Reducer)は純粋な関数にする。


ReSwift

今回、ReduxのSwift実装の


を使用して実装しています。
Redux原則を元にデータフローを制御することで、変更の反映漏れやデータの引き回しの不要になります。

ApplicationCoordinator

ライブラリに頼らない遷移管理パターンで、AppDelegate内でUIWindowを継承して初期化することで、遷移関連を統一した1ファイルで管理することが可能です。
ViewController内のDelegateの宣言が前提の為、一時的に渡したい変数等は簡単に引き継ぐことが可能です。

サンプル

今回はサンプルとして以下のような簡単な実装をReSwift + ApplicationCoordinatorで構築したサンプルコードを紹介します。
入力ボタンで入力専用画面へ遷移されて、入力した内容が最初の画面に反映されています。

実行環境

  • mac OS Sierra 10.12.6
  • Xcode 9.2
  • Swift 4
  • ReSwift 4.0.0

ReSwiftは、Cocoapodsで導入しています。

pod 'ReSwift'

Storyboard

Storyboardには簡単なUIコンポーネントを配置しています。

Model

最初にReSwiftの登場するState、Action、Reducerを定義します。
状態は、AppStateの中にツリー構造で定義します。Reducerは、MainStateのextensionで宣言することで、名前空間を制御しています。

//
//  AppState.swift
// AppDelgateで初期化し、全体のStateを管理する構造体です。
//
import Foundation
import ReSwift

struct AppState: StateType {
    var main = MainState()
}
//
//  MainState.swift
//  子State
//
import Foundation
import ReSwift

struct MainState: StateType {
    var text: String?
}
//
//  InputAction.swift
//  Actionの構造体です。変更したいStateの値を宣言しています。
//
import Foundation
import ReSwift

struct InputAction: Action {
    var text: String
}

//
//  AppReducer.swift
//  大本のReducerです。
//
import Foundation
import ReSwift

func AppReducer(action: Action, state: AppState?) -> AppState {
    return AppState(
        main: MainState.MainReducer(action: action, state: state?.main)
    )
}
//
//  MainReducer.swift
//  子のReducerで、今回は、InputAction構造体に反応して新しいStateを生成して返す処理をしています。
//
import Foundation
import ReSwift

extension MainState {
    static func MainReducer(action: Action, state: MainState?) -> MainState {
        let state = state ?? MainState(text: "")
        var newState = state
        var mainState = state
        switch action {
        case _ as InputAction:
            mainState = MainState(text: (action as! InputAction).text)
        default:
            break
        }
        newState = mainState
        return newState
    }
}

ApplicationCoordinator

本クラスで各ViewController上で定義した、protocolを使用して遷移を管理します。
AppDelgateで初期化する為、UIWindowをイニシャライザに持ちます。
ViewControllerのprotocolは、extensionで定義するのがオススメです。

//
//  ApplicationCoordinator.swift
//
import UIKit

final class ApplicationCoordinator {
    
    private let window: UIWindow
    
    init(window: UIWindow) {
        self.window = window
    }
    
    public func start() {
        guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "VC") as? ViewController else {
            return
        }
        vc.transitionDelegate = self
        self.window.rootViewController = UINavigationController(rootViewController: vc)
        self.window.makeKeyAndVisible()
    }
}

extension ApplicationCoordinator: ViewControllerTransitionDelegate {
    func push() {
        guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "SecondVC") as? SecondViewController  else {
            return
        }
        (self.window.rootViewController as? UINavigationController)?.pushViewController(vc, animated: true)
    }
}

AppDelegate

Storeの初期化とApplicationCoordinatorの初期化をします。


//
//  AppDelegate.swift
//

import UIKit
import ReSwift

// NOTE: アプリで1つのStoreを作成生成する
let mainStore = Store(
    reducer: AppReducer,
    state: nil
)

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    // NOTE: lazyで定義して、始めて使用されるタイミングでApplicationCoordinatorを初期化しています。
    lazy var applicationCoordinator: ApplicationCoordinator = {
        return ApplicationCoordinator(window: self.window!)
    }()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.applicationCoordinator.start()
        return true
    }
...

Controller

//
//  ViewController.swift
//
import UIKit
import ReSwift

protocol ViewControllerTransitionDelegate: class {
    func push()
}

final class ViewController: UIViewController {
    weak var transitionDelegate: ViewControllerTransitionDelegate?
    
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 監視作成
        // NOTE: 特定の状態のみ監視し、同じ値の場合はSkipする
        mainStore.subscribe(self) {
            $0.select {
                $0.main
                }.skipRepeats {
                    $0.text == $1.text
            }
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // 監視削除
        mainStore.unsubscribe(self)
    }
    
    @IBAction func event(_ sender: Any) {
        self.transitionDelegate?.push()
    }
}

extension ViewController: StoreSubscriber {
    typealias StoreSubscriberStateType = MainState
    // NOTE: 監視しているの値が変更されてた場合に呼ばれる
    func newState(state: MainState) {
        self.label.text = state.text ?? ""
    }
}


//
//  SecondViewController.swift
//
import UIKit
import ReSwift

final class SecondViewController: UIViewController {
    
    @IBOutlet weak var textField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.textField.text = mainStore.state.main.text
    }
}

extension SecondViewController: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) {
        // NOTE: dispatch関数を使用して、Actionを送信する。対応するReduserが検知し、新たなStateが作成され更新されます。
        mainStore.dispatch(InputAction(text: textField.text ?? ""))
    }
}

まとめ

iOS開発の課題を全て解決しているわけではありませんが、状態変化が起きた際の処理を一箇所にまとめるこで、可読性が改善し、処理が散らばらないのでデバッグしやすいと感じています。
興味のある方は実際に触ってみると面白いと思います。
ReSwiftの表面的な使い方やアーキテクチャ選定のお役に立てれば幸いです。
ここまでお読みいただきまして、ありがとうございました。


次回は社員インタビュー(若手編)を記事にさせていただく予定です。乞うご期待ください!