Continuous Corner Curveを自作する
この記事は「Eureka Advent Calendar 2020」の4日目の記事です。
3日目はAymenによる「Animations challenges #3 — Zenly Pops iOS Animation」でした。
エウレカのiOSエンジニアのMattoです。PairsのiOSアプリの開発を担当しております。
概要
iOS 7でアプリのアイコンに導入されたContinuous Corner Curveは角丸四角形を美しく描画することに優れています。
UIKitでContinuous Corner Curveを描画するにはCALayerCornerCurve.continuousとUIBezierPathの2通りの方法があります。
CALayerCornerCurveはiOS 13以降に提供されているため、iOS 13以降のみをサポートするのであれば、CALayerCornerShapeの.continuousを使うことで、あらゆるViewに対してContinuous Corner Curveを適用することができます。
一方で、iOS 12以前をサポートする場合にはUIBezierPathを使用する必要があります。しかし、UIBezierPathによるContinuous Curveが特定のケースで期待通りの挙動にならず、CALayerCornerShapeを用いた場合に見た目上の違いが生じてしまいます。 そのため、その事象が起こる条件下では、自前でAppleのContinuous Corner Curveを再現する必要があります。
本稿では、UIKitにおけるContinuous Corner Curveの再現についての調査とその難しさ、アプリでの対応策について述べます。
導入
iOS 7以降、アプリのアイコンの角丸が滑らかな曲線へと変わりました。
この角丸の特徴は、直線と曲線の交点が滑らかに繋がっていることです。 この角丸はMacbookやiPhoneの角にも適用されており、注意深く観察すると、頂点に尖りのないなめらかな角丸であることがわかると思います。
この曲線はSmooth CurveやContinuous Corner Curve、Squircle Cornerなどと呼ばれています。便宜上、本稿ではこの角丸のことをContinuous Corner Curveと呼ぶことにします。
Continuous Corner Curve を UIKit で描画する
UIKitでSmooth Curveを描画するには2種類の選択肢があります。
ひとつめはUIBezierPathを使うことです。 UIBezierPathをマスクにしてViewにContinuous Corner Curveを適用できます。 UIBezierPathでの描画は常にContinuous Corner Curveになります。
ふたつめはViewのLayerに対してCALayerCornerCurve.continuousを指定することです。 ただし、CALayerCornerCurveはiOS 13以降でしか指定できません。
UIBezierPathによる描画が期待通りの挙動にならないケース
UIBezierPathを使用した描画では、角丸が意図しないサイズにUIKitから変更されてしまう場合があります。 たとえば、同一のサイズの四角形のViewにそれぞれ同じ角丸のサイズを指定し、UIBezierPathとCALayerCornerCurveで描画してみます。 角丸のサイズを変更しながら列挙させてみると、以下のようになります。
UIBezierPathで描画したものは、radiusが30、35、40の時に同じ丸みになってしまっています。青い線で囲った部分が問題の部分になります。 サンプルコードは以下の通りです。 重要でない部分は省略しています。
正方形のViewに対しても同じことが起きます。
このように、辺の長さに対する角丸の割合が一定以上の場合にはUIBezierPathとCALayerCornerCurveでは結果が異なってしまいます。
おそらくですが、UIBezierPathで描画する際には、辺の長さが一定の割合より短い場合に、こちらで指定した値を上書きする挙動が組み込まれているようです。
手動二分探索してみたところ、80x80ピクセルのViewでは角丸が26.1ピクセル以上のときにこの現象が起こるようです。 この閾値の正確な値はわかりませんが、だいたい辺の長さの0.3倍くらいのところでこの現象が起こりそうです。
つまり、iOS 12以前においてはUIKitの機能を呼び出すだけでは、全てのViewに対してContinuous Corner Curveを期待通りに適用することができません。
iOSのContinuous Corner Curveを再現することは可能なのか?
前章で述べたとおり、iOS 12以前ではCALayerCornerCurveを指定できないため、UIBeizierPathを用いて描画するしかなく、辺の長さに対する角丸の割合が一定以上の場合にはContinuous Corner Curveを期待通りに適用することができません。 この場合、UIBezierPathでContinuous Corner Curveを再現して描画することが考えられます。
再現を試みた文献がいくつかWeb上で見つかりました。
[1] Exploring iOS 7 Rounded Corners
UIBezierPathの制御点と、それらを線で繋いだものを描画したものです。 この記事によって、UIBezierPathが制御点の数と位置が明らかになっています。 また、120x120ピクセルのアイコンにおいて、角丸を描画する制御点は四角形の頂点から27px引いた点から始まっていることが、図からわかっています。
[2] iOS Rounded Rect Script for Photoshop
前述のExploring iOS 7 Rounded Cornersで求められた割合を基に、再現を行ったPhotoshop Scriptです。 いくつかの定数からベジェ曲線を描画しています。
[3] Unleashing Genetic Algorithms on the iOS 7 Icon
また、彼は遺伝的アルゴリズム(GA)を用いて、UIBezierPathをアプリアイコンに近似させることにも挑戦しています。 結果として[2]よりも、より近い曲線を再現できたそうです。 今後の課題として、別の最適化手法を用いてさらなる精度の向上があると述べています。
[4] Desperately seeking squircles
この文献では、[1, 2, 3]とは別に、数学的にベジェ曲線を近似することを試みたものです。 AppleのContinuous Corner Curveから曲率を求め、クロソイド曲線(Clothoid)やスーパー楕円(Superelipse)のパラメータを変更したものと比較しています。 また、制御点の位置を決める式とそのパラメータを求め、オリジナルとかなり近い近似に成功しています。 さらに、figmaはここで得られた式と値を元に、figma自体にこの曲線を再現するオプションを導入しています。
[5] squircle-sketch
アプリのアイコンに採用されているGrid Systemを元に、再現をしたものです。 見た目上、ほぼ完璧に再現されているように見えます。 分析を進めれば、可変のCorner Radiusに対応させることが可能になるかもしれません。
完全再現できているものはないようですが、限りなく近いものは提案されています。
ライブラリ
前章で紹介した手法を自分で実装するのは大変なので、ライブラリをいくつか紹介します。
こちらは[3]で得られた定数を基にして公開されているライブラリだと思われます。
こちらは中身をあまり読んでいないのでわかりませんが、描画された結果を見る限り[3]を基にしていないかもしれません。
結論
iOS 13以降のみをサポートするアプリの場合、CALayerCornerShapeの.continuousを使うことで、あらゆるViewに対してContinuous Corner Curveを適用することができます。
iOS 12以前のUIKitで提供されているUIKitでは、UIBezierPathによるContinuous Curveが特定のケースで期待通りの挙動になりません。 iOS 12以前をサポートする場合、UIBezierPathを用いて、辺の長さに対する角丸の割合が一定以上ならサードパーティのライブラリを用いることで適用が可能です。 すでに公開されているライブラリには、観測している限りではありますが、完全にUIBezierPathと一致するContinuous Corner Curveをサポートするものはありません。 ただし、サードパーティのライブラリは完全にUIBezierPathとは同じではなく、場合によっては違和感を持つため、可能な場合にはUIBezierPathにフォールバックして表示するのが良いと思います。 OSSのライブラリにはこれらのパラメータはなんのデータを基に決定したか公開されていないものや、オリジナルとはかけ離れた結果になるものがあるため、使用する際には違和感がないか、または画像を重ねてズレが大きくないか、確認することが望ましいです。
また、自前で完全に再現したものを用意することは非常に難しいと考えられます。
付録: SwiftUIのPathからUIBezierPathを描画してみる
SwiftUIのPathを用いて描画することで、なにかヒントを得られないかと考え、UIBezierPathに変換したものを描画してみました。 菱形が.move、四角形が.addCurve、円形が.addLineを描くための座標を表しています。 QuadCurveは使用されていませんでした。
パラメータと描画するものが多いので相当わかりづらいです。
まず、この図からわかることして、ひとつの角に対して3つのカーブが適用されています。 これは確かに完全再現は難しそうです。
ひとつ気になる点として、角丸の部分に同じ座標でaddLineが適用されています。 描画に失敗したのかと思いましたが、確かにaddLineが使用されており、意図は不明です。 なんらかの繰り返し処理に組み込まれており、一見無意味に見えるのかもしれません。
今後の課題として、こちらをさらに解析すれば、指定したCorner Radiusと、実際に描画されている曲線の開始位置から比率を求めることで、再現が可能になるかもしれません。
また、インタフェースや挙動を見る限り、SwiftUIのPathはUIBezierPathをラップしたもののようです。 そのため、SwiftUI.Pathで描画したものは先述の意図しない挙動が起こります。 SwiftUIでContinuous Corner Curveを描画したい際には注意が必要です。
エウレカでは、ビジネス成長を技術面から支え、加速させることに興味関心があるiOSエンジニアを募集しています。