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コード読んでみたら、渡したファイルパスの存在チェックとかしてなさそうだったので、だったらオプション渡せばいけるじゃんとなりました。

find+xargsのよくある処理で空白を含む名前のファイルを扱う

はじめに

findxargsを使って、複数ファイルをまとめて処理するのはよくやると思います。

が、とくにオプションとか考えずにやると空白混じりのファイル名があった時に、エラーで怒られます。

space_files $ ls -la
total 24
drwxr-xr-x   5 sumida  staff   170  2 25 18:26 .
drwxr-xr-x  52 sumida  staff  1768  2 25 18:18 ..
-rw-r--r--   1 sumida  staff   331  2 25 18:24 hoge 1.txt
-rw-r--r--   1 sumida  staff   515  2 25 18:24 hoge2.txt
-rw-r--r--   1 sumida  staff   689  2 25 18:26 hoge3.txt
space_files $ find . -type f|xargs wc -l
wc: ./hoge: open: No such file or directory
wc: 1.txt: open: No such file or directory
      13 ./hoge2.txt
      25 ./hoge3.txt
      38 total
space_files $

これは、xargsが空白文字を区切り文字として使用するためです。

上の例だと、wc -l hoge 1.txtとなることを期待していますが、実際にはwc -l hogewc -l 1.txtに分割されてしまい、そんなファイルはないので怒られます。

解決策

find-print0オプションとxargs-0オプションを指定すれば解決。

space_files $ find . -type f -print0 |xargs -0 wc -l
       8 ./hoge 1.txt
      13 ./hoge2.txt
      25 ./hoge3.txt
      46 total
space_files $

このオプションは、二つとも区切り文字をヌル文字にするオプションなので、空白が含まれていても1つのファイル名として扱ってくれます。

画像とラベルを縦に並べるUIButtonのカスタムクラス

はじめに

たまに必要になるので、こんな感じに画像とラベルを縦に並べるUIButtonのカスタムクラスを作りました。

f:id:y_sumida:20180122225341p:plain

contentEdgeInsetsとか毎回調整するのめんどくさいですよね。

探せばライブラリとかもあるとは思うけど勉強も兼ねて車輪の再発明です。

コード

github.com

画像とラベルのサイズに合わせて、縦に並べてセンタリングします。

Storyboardからもコードからも使えるようになってます。

コードからは上下と左右のマージンが指定できます。

不具合

  • width指定、leadingEdge/trailingEdge指定などで、必要な幅より狭い幅の場合に表示が崩れる。
    • いろいろ試したけど、今のところうまくいってない。
    • drawに入ってきた時点でラベルの後ろが切れてたりするので。

感想など

  • もうちょっとスマートに書きたかった。
  • contentEdgeInsetsとかの親クラスの変数を変更不可にするのに手間取った。
  • UIButtonに縦並びとかアイコンとラベル入れ替えるオプション欲しい。

参考

UIViewのライフサイクルについて、以下の記事が大変参考になりました。

developers.eure.jp