角丸が尖る問題の直し方

f:id:mizonit:20210717002316p:plain

こんにちは。 英単語アプリmikanのiOS開発をやっている、aviciidaです。

最近嬉しかったことは、iOSDC 2021に提出したレギュラートークのプロポーザルが採択されたことです。

ど文系でエンジニア未経験の状態から開発の世界に入り、かれこれ2年が経ちましたが、ようやくこれで自分もエンジニアです、と胸を張って言えるなと思って嬉しいです。

角丸が尖ってしまう問題を解決する方法

今回は、iOS開発で以前に詰まってしまった「角丸が尖ってしまう問題」をどうやって解決したか、という話をしたいと思います。

そもそも、どんな問題なのか

写真を見てもらうとわかりやすいと思います。

f:id:aviciida:20210716152412p:plain

学習のボタンが尖ってしまってますね。

発生しやすいパターンは

  • 小さな端末(iPhone SEなど)
  • 多重構造(UIView in UICollectionView in UIView in UIViewみたいな)

という感じでした。

mikanではこれを「ラグビーボール問題」と呼んでいました。

端的に言うと、cornerRadiusをheight / 2にしている時に、height / 2がobjectの実際のheight / 2よりも大きくなっていることが原因です。

そもそも、cornerRadiusと角丸の関係はどうなっているかというと、 あるobjectの角っちょを、cornerRadiusを半径とした円で切り抜くよ、そこが角丸になるよ、ということです。 cornerRadiusを、そのobjectのheigt / 2に指定すると、このようになります。

f:id:aviciida:20210716152457p:plain

しかしそれがheightの半分を超えてしまうと f:id:aviciida:20210716152522p:plain

このように、先っちょが尖ってしまいます。

なぜこのようなことが起きるのか

これも簡単に言うと、viewのcornerRadiusを指定するために高さを取得しようとするとview.frame.heightで取得すると思いますが、その値が間違っているということです。

例えば、この画像の例でいくと、

f:id:aviciida:20210716152412p:plain

本当は、view.frame.height31.0になるはずでした。

しかし、このviewの(Auto Layoutの制約がresolveされる)layoutSubViews()view.frame.heightをprintしてみると f:id:aviciida:20210716152645p:plain

Interface Builderで指定したサイズのままになってしまっています。

対策① CustomViewを作る

layoutは親viewから子viewに、という順番なので、今回問題になっているviewのサイズは、1番親のviewのlayoutSubview()時点ではまだ決められていないのでは、という仮説を立てました。

問題のviewは、階層構造のかなり下の方にありました。

f:id:aviciida:20210716152659p:plain

そこで、問題のviewをCustom Viewに切り出して、そこのlayoutSubviews()であれば、正しいheightを取得できるのではと考えました。 結果...

f:id:aviciida:20210716152708p:plain

無事取得できました!

対策② layoutIfNeeded()を呼び出す

親のviewを生成しているところでlayoutIfNeeded()を呼び出し、viewのクラス内でlayoutIfNeeded()をオーバーライドし、その中でheightを取ることによってでもうまく取得することができました。

layoutIfNeeded()のドキュメントを見てみると

Use this method to force the view to update its layout immediately. 
When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. 
Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. 

(雑に翻訳)
レイアウトの更新を強制的に行うために呼び出す。
Auto Layoutを使用している場合は、レイアウトエンジンが、制約の変更に対応するために必要なviewのポジションの調整を行う。
この処理が走ったら、処理対象のviewをroot viewにして、それよりも下の階層にあるviewがレイアウトされる。

つまり、強制的に制約を更新してレイアウトを行ってくれるfunctionなので、これをオーバーライドした中でheightを取得すれば正しい値が取れるということかと思います。

まとめ

ということで、今回は「ラグビーボール問題」をどうやって倒したかを解説しました。

要は、多重階層になっているviewの子viewのheightがlayoutSubviews()でも間違った値が入ることがあるので、そういう時には子viewのCustom Viewを作ったり、layoutIfNeeded()を呼び出したりして正しいheightをとりましょうね、という感じでした!

今見ると当たり前では?という感じですが、当時はまだ経験が浅く、かなり倒すのに時間がかかってしまいました。

mikanは絶賛採用中です! ご興味ある方は、お気軽にご連絡ください! mikanの採用情報

参考にした記事

Subview frame is incorrect when creating UICollectionViewCell

UIKitのView表示ライフサイクルを理解する