Help us understand the problem. What is going on with this article?

iOS 13 端末で UIScrollView の Indicator に画像を設定するコード部分がクラッシュしていたのでその対応

More than 1 year has passed since last update.

はじめに

iOS 13 がリリースされましたね。
例年になく大きな変更があり,皆さん様々な対応を行ったことと思います。
私も主に個人アプリで対応してギリギリ 9/20 にリリースできました。

今回はその iOS 13 対応中に見つけたクラッシュとその対応の記事になります。

iOS 13 で UIScrollView の Indicator の仕様が変わった?

UIScrollView の Indicator ってありますよね。
スクロールした際に右端に表示されるもの(今回の話は Vertical Indicator の方)です。
そのスクロールバーに下記の画像を設定していました。

scrollBarImage@3x.png

iOS 12 までは下記のようなコードで動いていました。
(今見ると恐いな・・・😓)

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let verticalScrollIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as! UIImageView)
        verticalScrollIndicator.image = UIImage(named: "scrollBarImage")
    }
}

RPReplay_Final1569158455.gif

しかし,このコードだと iOS 13 の端末でスクロールしようとしたらクラッシュします。
クラッシュした際のログは下記の通りです。

Could not cast value of type '_UIScrollViewScrollIndicator' (0x7fff895ad0b0) to 'UIImageView' (0x7fff895cde00).

UIImageView で受けられなくなっているようで,
View Hierarchy で確認してみると・・・

iOS 12 の場合は,インジケータは UIImageView になっている。
こいつを取得して画像をセットしている。

scrollbar_iOS12.png

iOS 13 端末だと UIView になっている。
なるほど UIImageView でキャストできずクラッシュするわけだ。

scrollbar_iOS13.png

iOS 13 での対応

  • Xcode 11
  • iOS 12, 13

サンプルコードは GitHub にあります。気になる方はご覧ください。
https://github.com/MilanistaDev/ScrollBarWithImage

UIView になっているなら,
単純に UIImageViewaddSubView すればいいと考えました。
AutoLayout も使って Indicator いっぱいに画像を表示させます。

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // スクロールバーに画像をセット
        if let verticalScrollIndicator: UIView = scrollView.subviews.last {
            let scrollBarImageView = UIImageView.init(frame: .zero)
            scrollBarImageView.contentMode = .scaleToFill
            scrollBarImageView.image = UIImage(named: "scrollBarImage")
            scrollBarImageView.translatesAutoresizingMaskIntoConstraints = false
            verticalScrollIndicator.addSubview(scrollBarImageView)

            // AutoLayout で ScrollBar いっぱいに画像を設置
            scrollBarImageView.leadingAnchor.constraint(
                equalTo: verticalScrollIndicator.leadingAnchor,
                constant: 0.0
                ).isActive = true
            scrollBarImageView.bottomAnchor.constraint(
                equalTo: verticalScrollIndicator.bottomAnchor,
                constant: 0.0
                ).isActive = true
            scrollBarImageView.trailingAnchor.constraint(
                equalTo: verticalScrollIndicator.trailingAnchor,
                constant: 0.0
                ).isActive = true
            scrollBarImageView.topAnchor.constraint(
                equalTo: verticalScrollIndicator.topAnchor,
                constant: 0.0
            ).isActive = true
        }
    }
}

これでうまくいきますが,このままだと大変なことになっているだろうなと🤔
画面を見てもわからないけど,やはりビーム出してますね。。。

scrollbar_beam.png

addSubView された画像を都度削除するために,
Indicator の subViews を削除する処理を入れます。

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // スクロールバーに画像をセット
        if let verticalScrollIndicator: UIView = scrollView.subviews.last {
            // 追加した画像があれば削除
            let subviews = verticalScrollIndicator.subviews
            for subview in subviews {
                subview.removeFromSuperview()
            }
            let scrollBarImageView = UIImageView.init(frame: .zero)
            scrollBarImageView.contentMode = .scaleToFill
            scrollBarImageView.image = UIImage(named: "scrollBarImage")
            scrollBarImageView.translatesAutoresizingMaskIntoConstraints = false
            verticalScrollIndicator.addSubview(scrollBarImageView)
            // ScrollBar いっぱいに画像を設置
            scrollBarImageView.leadingAnchor.constraint(
                equalTo: verticalScrollIndicator.leadingAnchor,
                constant: 0.0
                ).isActive = true
            scrollBarImageView.bottomAnchor.constraint(
                equalTo: verticalScrollIndicator.bottomAnchor,
                constant: 0.0
                ).isActive = true
            scrollBarImageView.trailingAnchor.constraint(
                equalTo: verticalScrollIndicator.trailingAnchor,
                constant: 0.0
                ).isActive = true
            scrollBarImageView.topAnchor.constraint(
                equalTo: verticalScrollIndicator.topAnchor,
                constant: 0.0
            ).isActive = true
        }
    }
}

これでうまくいきました!

RPReplay_Final1569165176.gif

(おまけ)Indicator の BackgroundColor を設定する?

今回は画像を addSubView しているので,
ライトモードでもダークモードでも同じ色になります(私はこうしたかった)。
少し色味を変えたいのであれば,
アセットでライトモード用の画像とダークモード用の画像を用意すればいいですね。

ライトモード ダークモード
lightmode.png darkmode.png

単色などであれば,取得した Indicator の UIView
BackgroundColor を設定すれば良いとも思うんですが,
どうも UIScrollViewIndicatorStyle の設定があるためか綺麗な色が出せません。
単純に設定しない .default だと
ライトモード では .blackダークモード では .white が設定されているように見えます。

試しに UIColor.red を設定してみたのが下記になります。

ライトモード ダークモード
scrollbar_light_red.png scrollbar_dark_red.png

UIScrollView の Indicator を触るような実装をすることは
ほとんどないとは思いますが,画像で対応したほうが綺麗に対応できると思いました。

おわりに

今回は,iOS 13 対応中に出た UIScrollView の Indicator 周りのバグについて書きました。
iOS 13 単体の不具合が多い気がします。iOS 13.1 のリリースが待たれますね。

こうしたほうが良い,私はこうしてるよー等ありましたらご教示お願いいたします🙇‍♀️
ご覧いただきありがとうございました。

MilanistaDev
業務・個人でiOSアプリの開発しています。Apple大好き。WWDCは2016-19の4回参加し,様々な新技術・エンジニアに出会って一層頑張らないと思い直しました。日々勉強の精神でQiita・ブログ執筆,LT登壇などアウトプットを大事にしています。アプリ開発はUI作るのが特に好き。最近はSwiftUIを色々触っています。アプリ設計,テスト周りがまだまだ弱いので力入れたい。SES->受託開発->??
http://milanista224.blogspot.jp
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc
http://www.yumemi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away