iOSDC2021に参加しました

去年に引き続きオンラインでの開催ということで、毎年恒例の雨でも影響ないのが良いところですね。

前夜祭には参加できなかったのですが、本編2日間楽しかったです。

セッションはどれも気になる内容ばかりで選ぶのが大変でした。

無料ユーザーでもタイムシフト予約できるようにしていただいており、後から見返すことができてありがたいです。

全部面白かったけど、特に印象に残ったセッションをメモしておきます。

機能ごとに動作するミニアプリでプレビューサイクルを爆速にした話

speakerdeck.com

大規模なアプリのマルチモジュール構成の実践

speakerdeck.com

いろいろなところで見るクックパッドさんのマルチモジュールのお話。

モジュール分割の技術的な面だけでなく、社内に浸透させるための取組みのお話が大変よかったです。

開発系の話にインセンティブという概念が出てくるのが印象的でした。

Discordもよかった。

f:id:y_sumida:20210920233141p:plain

Swift Package中心のプロジェクト構成とその実践

speakerdeck.com

このセッションは本当によかったです。

前提知識、用語、概念の説明にかなりの時間を割いていて、とても丁寧な発表だったと思います。

パッケージマネージャーを活用してプロジェクトファイルのスリム化、さらにマルチモジュールのミニアプリ化、マルチプロジェクトによる環境の切り替えに繋がっていくところは目から鱗が落ちまくりでした。

SPMを勉強しつつ、モジュール分割できるように疎結合な状態にしていきたでいです。

アンカンファレンス

fortee.jp

14年もののObjective-Cのコードを動かそうという試み。

当時のお話、懐かしい古いiPhoneの画面、エスパーのような修正箇所の指摘などなど、大いに盛り上がって見ていて楽しかったです。

最後、きっちり動いて終わったのが本当によかった。

iOSDCチャレンジ

毎度お馴染みトークン探し。

パンフレットだけでもコンプリートしようと目を皿のようにして眺めていましたが、その他ページがどうしても100%にならずタイムアップ。

ところがクロージング眺めてたら、なんと88位でキリ番という奇跡。ありがとうございました。

また来年!

スタッフの皆さん、参加者の皆さん、おつかれさまでした。

来年はオフライン開催できるといいなあ。

iOSDC Japan 2019 に行ってきた

9/6,7 に開催された iOSDC 2019 に参加してきました。

iosdc.jp

印象に残ったセッション

参加したセッション、LTすべて楽しかったです。

毎度のことながら、全部参加できたらいいのにという状態でした。

参加できなかったセッションについても、動画公開されたら見てみようと思います。

以下、印象に残ったセッションです。

ライブラリのインポートとリンクの仕組み完全解説

speakerdeck.com

TrackAの大きなホールなのに立ち見が出るほどの盛況でした。

基礎的なところから丁寧な説明でとてもわかりやすかったです。

問題の切り分けフローはとても役に立ちそうです。

時間の関係で省略された応用編もどこかで聞いてみたいですね。

実機の管理とおさらば!AWS Device FarmでiOSのテストをしよう!

speakerdeck.com

こちらも立ち見が出るほど大盛況のセッションでした。

AWS Device Farmの紹介と思いきや、XCTestを使ったUnitTest、UITestの基礎から始まって、AWS Device Farmでのテスト、CI/CDツールとの連携まで盛りだくさんな内容でした。

iOSアプリのテスト書いたことない、難しそうみたいに思ってる人は、この資料とセッション動画を見てみることをおすすめします。

Ask the Speaker でお話しできたのもよかったです。

iOSアプリのリジェクトリスクを早期に発見するための取り組み

speakerdeck.com

QAチームが、リジェクト可能性を事前にできる限り潰しているのは強いなあと思いました。

ipaファイルの中身をチェックするツールを作るという発想が素晴らしい。

オープンソースにならないかなー。

SOLID原則を生活に適用する

speakerdeck.com

人生設計にもSOLIDを適用できるのではというお話。

"疎結合な人生" で笑ってしまいました。

個人的にはLが一番むずかしいと思います。

テストケースで Ambiguous Layout を発見する

https://www.icloud.com/keynote/0nUzX497oPS1WvIIOwV1MODLg#iOSDC2019

テストでAmbiguous Layoutが検知できるライブラリ XCTAssertAutolayout がエラーも見やすくてとても良さそうでした。

突然低レベルプログラミングに突入していくのが面白かったです。

ご本人にとっては不本意かもしれませんが、デモ中に発生したアクシデントにも冷静に対処されていて、ライブデバッギングを見ることができたのも良かったです。

自作して理解するリアクティブプログラミングフレームワーク

https://fortee.jp/iosdc-japan-2019/proposal/a4e78fe8-6adb-4a92-a232-256ac0fa7976

RxSwiftを車輪の再発明することでリアクティブプログラミングを理解するというセッション。

会場のツイートがスライド上に流れていくまさに双方向なセッションでした。

RxSwiftもどきを作るために必要なTODOを順に潰していく、TDDのお手本のような進め方を動画で見せるという形式がとてもよかったです。

その他

セッションの合間にスポンサーブースを周りました。

WantedlyさんのブースではSwiftクイズを解いてSwiftUI本をゲットできてよかったです。

来年はスポンサー参加したいですね。

感想

今年もめちゃくちゃ楽しかったです。

印象に残った良かった点をいくつか上げてみます。

セッションの予約がよかった

今回初の試みとしてセッションの予約(サポーターorジョーカーのみ)ができました。

予約なのでギリギリに入室しても全て最前列で聞くことができて満足度が高かったです。

反面、立ち見が出るような状況において、予約席に空きがあったりしたのはもったいないなあという感じでした。

運営がスムーズで素晴らしい

相変わらず運営スタッフの練度が高く、とにかく運営がスムーズな印象でした

何かあったらすぐ対応してもらえる安心感。

ネットワークが素晴らしかった

短時間つながらないタイミングなどありましたが、概ね快適なネットワークでした。

1000人規模のイベントでこれはすごいことなのでは。

最終日のLTもおもしろかったです。

おまけ

2日間子どもたちの面倒を見てくれて、快く送り出してくれた妻に感謝。

2018年振り返り

今年も無事終わってよかった。

お仕事

  • 年明けくらいからiOSチームのリーダー&Androidチームのタスク管理とかするようになった
    • 自分でもコード書くけど、若者が手を動かしやすいように雪かき業とか進んでやってる
    • 何度かのリリースを無事に終えられて良かった
  • 仕様書をgithubで管理したり、振り返りをするようにしたり、開発プロセスを少しずつ改善中
  • iOSチームは定期的にモブプロするようになった
  • 英文メールでやりとりしてBitriseの訪問受けたのは面白かった

子育て

勉強とか

  • iOSDC他、iOS関連の勉強会にいくつか参加した
    • 渋谷開催だと会社帰りに参加できて便利
  • 本は7冊読了、読みかけが2冊くらい。
    • 『Go言語で作るインタプリタ』、『エンジニアリング組織論への招待』が面白かった

Go言語でつくるインタプリタ

Go言語でつくるインタプリタ

  • 英語勉強しようと思ってスタディサプリを始めた
  • 草は去年より多いけど、後半やっぱり少なくなりがち

f:id:y_sumida:20181231174120p:plain

来年の目標

  • 毎日少しでも本読んだりコード書いたりする
  • 業務プロセスとかいい感じに改善して、サービスをもっと良くしていきたい

ちょっと目標が低い気がするけど、子育てメインでまとまった時間とれないので、こんなもんで。

UICollectionViewのスクロールバーがセクションヘッダに隠れないようにする

環境

iOS12.0
Xcode Version 10.0 (10A255)
Swift4.2

問題

UICollectionViewのセクションヘッダを表示する必要があって、実装してみたらスクロールバーがセクションヘッダの下に隠れてしまったのでなんとかしたかった。

f:id:y_sumida:20181219224848p:plain

解決策

UICollectionViewDelegateの collectionView(_:willDisplaySupplementaryView:forElementKind:at:) で zPosition を指定することで解消できた。

extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
        view.layer.zPosition = 0.0
    }
}

f:id:y_sumida:20181219224917p:plain

リポジトリ

GitHub - y-sumida/UICollectionViewHeaderFooterSample: UICollectionViewにヘッダ・フッタを表示する

慣れてないのもあるけど、UITableViewに比べてUICollectionViewは癖が強い気がする。

参考

collectionView(_:willDisplaySupplementaryView:forElementKind:at:) - UICollectionViewDelegate | Apple Developer Documentation

UIPageControllerの選択しているドットを大きくする

環境

iOS12.0
Xcode Version 10.0 (10A255)
Swift4.2

やりたいこと

こんな感じにUIPageControllerの選択中のドットを大きくしたい

f:id:y_sumida:20181215231319p:plain

カスタマイズしたUIPageCotrollクラス

class CustomPageControl: UIPageControl {
    private static let defaultDotSize: CGFloat = 7.0 // UIPageControllerのドットのサイズ

    private lazy var currentPageIndicator: UIImage = UIImage.dotImage(color: .gray, size: CGSize(width: currentPageIndicatorSize, height: currentPageIndicatorSize))
    private lazy var pageIndicator: UIImage = UIImage.dotImage(color: .gray, size: CGSize(width: pageIndicatorSize, height: pageIndicatorSize))

    private lazy var currentPageIndicatorOffset: CGFloat = calcIndicatorOffset(size: CustomPageControl.defaultDotSize)
    private lazy var pageIndicatorOffset: CGFloat = calcIndicatorOffset(size: CustomPageControl.defaultDotSize)

    var currentPageIndicatorSize: CGFloat = defaultDotSize {
        didSet {
            currentPageIndicatorOffset = calcIndicatorOffset(size: currentPageIndicatorSize)
            currentPageIndicator = UIImage.dotImage(color: currentPageIndicatorTintColor ?? .gray, size: CGSize(width: currentPageIndicatorSize, height: currentPageIndicatorSize))
            updateDots()
        }
    }
    var pageIndicatorSize: CGFloat = defaultDotSize {
        didSet {
            pageIndicatorOffset = calcIndicatorOffset(size: pageIndicatorSize)
            pageIndicator = UIImage.dotImage(color: pageIndicatorTintColor ?? .gray, size: CGSize(width: pageIndicatorSize, height: pageIndicatorSize))
            updateDots()
        }
    }

    override var numberOfPages: Int {
        didSet {
            updateDots()
        }
    }

    override var currentPage: Int {
        didSet {
            updateDots()
        }
    }

    override var currentPageIndicatorTintColor: UIColor? {
        didSet {
            if let color = currentPageIndicatorTintColor {
                currentPageIndicator = UIImage.dotImage(color: color, size: CGSize(width: currentPageIndicatorSize, height: currentPageIndicatorSize))
            }
        }
    }

    override var pageIndicatorTintColor: UIColor? {
        didSet {
            if let color = pageIndicatorTintColor {
                pageIndicator = UIImage.dotImage(color: color, size: CGSize(width: pageIndicatorSize, height: pageIndicatorSize))
            }
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        clipsToBounds = false
        updateDots()

        addTarget(self, action: #selector(didChangeValue(sender:)), for: .valueChanged)
    }
}

extension CustomPageControl {
    private func updateDots() {
        var i = 0
        let currentPageIndicatorRect = CGRect(x: 0, y: 0, width: currentPageIndicatorSize, height: currentPageIndicatorSize)
        let pageIndicatorRect = CGRect(x: 0, y: 0, width: pageIndicatorSize, height: pageIndicatorSize)

        for view in subviews {
            if let imageView = imageForSubview(view) {
                if i == currentPage {
                    imageView.image = currentPageIndicator
                    imageView.frame = currentPageIndicatorRect
                    imageView.frame.origin.y = imageView.frame.origin.y - currentPageIndicatorOffset
                    imageView.frame.origin.x = imageView.frame.origin.x - currentPageIndicatorOffset
                } else {
                    imageView.image = pageIndicator
                    imageView.frame = pageIndicatorRect
                    imageView.frame.origin.y = imageView.frame.origin.y - pageIndicatorOffset
                    imageView.frame.origin.x = imageView.frame.origin.x - pageIndicatorOffset
                }
                i += 1
            } else {
                var dotImage = pageIndicator
                if i == currentPage {
                    dotImage = currentPageIndicator
                }
                view.clipsToBounds = false
                let addedImageView: UIImageView = UIImageView(image: dotImage)
                if dotImage == currentPageIndicator {
                    addedImageView.frame = currentPageIndicatorRect
                    addedImageView.frame.origin.y = addedImageView.frame.origin.y - currentPageIndicatorOffset
                    addedImageView.frame.origin.x = addedImageView.frame.origin.x - currentPageIndicatorOffset
                } else {
                    addedImageView.frame.origin.y = addedImageView.frame.origin.y - pageIndicatorOffset
                    addedImageView.frame.origin.x = addedImageView.frame.origin.x - pageIndicatorOffset
                }
                view.addSubview(addedImageView)
                i += 1
            }
        }
    }

    private func imageForSubview(_ view:UIView) -> UIImageView? {
        var dot: UIImageView?
        if let dotImageView = view as? UIImageView {
            dot = dotImageView
        } else {
            for foundView in view.subviews {
                if let imageView = foundView as? UIImageView {
                    dot = imageView
                    break
                }
            }
        }
        return dot
    }

    private func calcIndicatorOffset(size: CGFloat) -> CGFloat {
        return abs(size - CustomPageControl.defaultDotSize) / 2
    }

    @objc private func didChangeValue(sender: UIPageControl){
        currentPage = sender.currentPage
    }
}

使い方

  • StoryBoard上でUIPageController置いてこのクラスを指定する。コード上で生成も可。
  • viewDidLoad() とかで、currentPageIndicatorSize でカレントのドットサイズを指定する
  • カレント以外のドットのサイズも変えたい場合は、 pageIndicatorSize を指定する

ポイント

  • ドット用のUIImageを用意して、カレントページが変わるたびに差し替え
    • 塗りつぶした画像生成用のextensionをUIImageに生やしてます
  • ドットのサイズに合わせて位置調整をして、元のドットの中心と合わせるようにした
    • 単純にサイズ変えてるサンプルが多かった
  • タップアクションも忘れずに受けるようにした
    • けど、このやり方でいいのかは不明

その他

  • UIPageControllerをカスタマイズできるライブラリの多くは、UIPageControllerのカスタマイズじゃなくて独自のクラスで実現していた
  • でもドットの大きさかえるためだけにライブラリ入れたくないので良し

リポジトリ

GitHub - y-sumida/CustomDotPageControlSample: UIPageControlのドットの色やサイズを変えるサンプル

参考

stackoverflow.com

ナビゲーションのタイトルカラーを画面ごとに変更する

バージョン

  • Xcode Version 10.0 (10A255)
  • Swift4.2

やりたいこと

  • 最初の画面はタイトルカラー赤
  • 遷移先の画面はタイトルカラー青

実装1

ググるととよく出てくるやつ。

ただ、これだと最初の画面に戻った時に青のままです。

遷移元ViewController

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "ViewController1"
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.red]
    }

    // 省略
}

遷移先ViewController

class ViewController2: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "ViewController2"
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.blue]
    }

    // 省略
}

実装2

同僚氏に教えてもらいました。

遷移先の willMove(toParent:) で元の色に戻してやれば、最初の画面に戻った時に赤に戻ります。

遷移元ViewController

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "ViewController1"
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.red]
    }

    // 省略
}

遷移先ViewController

class ViewController2: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "ViewController2"
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.blue]
    }

    override func willMove(toParent parent: UIViewController?) {
        super.willMove(toParent: parent)
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.blue]
    }

    // 省略
}

他にもいろいろ試行錯誤したけど、これが一番まともでした。

ほんとは遷移してきた時点で前の画面のタイトルカラーを保存して戻してやりたかったんだけど、遷移してきたときには nil っぽかった。

なんかいい方法知ってたら教えてください。

試行錯誤の残骸です。

GitHub - y-sumida/NavigationTitleColorSample: ナビゲーションバーのタイトルカラーをページごとに変更する

Swiftのランタイムエラーを捕捉する

アプリがクラッシュした際に、ちょっとした後片付け的な処理をしたくて調べてた。

バージョン

NSSetUncaughtExceptionHandler

ググると、これがよく引っかかる。

NSSetUncaughtExceptionHandler(_:) - Foundation | Apple Developer Documentation

ただ、NSExceptionは捕捉できるけどSwiftランタイムエラーは捕捉できないっぽい。

ios - How to catch a Swift crash and do some logging - Stack Overflow

あと、うまく実装しないとCrashlyticsとかのハンドラーとかち合うみたい。

Signal

Stack Overflow でみつけたやつ。

Appleアーカイブっぽいところに少しドキュメントがあった。

Technical Note TN2151: Understanding and Analyzing Application Crash Reports

この辺の記事も参考になった。 qiita.com

harasou.jp

        let SignalHandler : @convention(c) (Int32) -> Void = {
            (signal) -> Void in
            let log = "signal \(signal)"
            UserDefaults.standard.setValue(log, forKey: "signal1")
            sleep(1)
            exit(EXIT_FAILURE)
        }
        
        signal(SIGABRT, SignalHandler)
        signal(SIGILL, SignalHandler)
        signal(SIGSEGV, SignalHandler)
        signal(SIGFPE, SignalHandler)
        signal(SIGBUS, SignalHandler)
        signal(SIGPIPE, SignalHandler)
        signal(SIGTRAP, SignalHandler)

こんな感じでOSのシグナルをハンドリングできる。

Swiftというよりシステムプログラミングっぽくなった。

結局、今回はアプリがバックグラウンドに行くときと、フォアグラウンドへの復帰時などの処理を見直して対応した。

サンプルコード

github.com