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

ユニバーサルリンクでiOSアプリが開けない場合の対策

アプリ開発中に、デザイナーさんが作ってくれたプロトタイプをProttViewerで開こうとして発生した事象です。

困っていたら、同僚がRadarのリンクを教えてくれて解決しました。感謝。

ProttViewerで発生した事象ですが、他のアプリでも発生します。

iOSバージョン

  • 11.4

事象

  • プロトタイプとして共有されたリンクをタップ
  • Safariに飛ばされて、ProttViewerで開くボタンがあるのでタップ
    • 本来は直接アプリが開く
  • AppStoreを開きますかと出るのでタップ
  • AppStoreに遷移して開くボタンタップ
  • ProttViewer開くけど何もない

もちろん、ProttViewerがインストール済みの端末での事象です。

対策

  • ProttViewerをアンインストール
  • 端末を再起動
  • ProttViewerをインストール

単にアプリの再インストールだけでも解決する場合もあるようです。

参考

fastlane で空のgit commitを作る

はじめに

fastlane で git commit --allow-empty が必要になってやり方を調べました。

バージョン

fastlane 2.85.0

方法

以下のどちらかで。他にもあるかもしれない。

# test.rb
lane :test do
  # Actions.sh 使う方法
  Actions.sh('git commit --allow-empty -m"Actions.sh"')

  # git_commit 使う方法
  git_commit(path:"--allow-empty", message:"git_commit")
end

おまけ

fastlane 使い慣れてないので、最初はActions.sh() を思いつかず、git_commit アクションにはそんなオプションなくて、困ったなーとなりました。

git_commit - fastlane docs

同僚氏にプルリクしようとか煽られつつ、慣れないRubyコード読んでみたら、渡したファイルパスの存在チェックとかしてなさそうだったので、だったらオプション渡せばいけるじゃんとなりました。