スーパー楕円UIをiOS+Swiftで実装する

2021年1月29日
Swift / iOS / UI /

image

弊社デザイナーの @kudakuargeスーパー楕円に関する良記事 を投稿していました。

スーパー楕円は最近話題になっているClubhouseでも使われているとのこと。

clubhouse

そのため便乗してiOS+Swiftでスーパー楕円UIを実装してみます。

どう実装する?

iOSアプリの上で上にUIImageViewとか様々なViewをのせるような使い方をすることになりそうですので、基本的にはUIViewのサブクラスである必要がありそうです。

スーパー楕円を表示(描画)するだけならUIBezierPathなどでスーパー楕円を作って UIViewのdrawメソッド をオーバーライドしてfillするなどで良さそうです。

しかし、上のUIImageViewなどをのせて、上にのせたViewも一緒にスーパー楕円でマスクされないといけないので、 CALayerのmask でスーパー楕円の形にマスクすべきかもしれません。

スーパー楕円はどう作る?

上の記事 にJavaScriptのサンプルコードがありますが、これはベジェ曲線での描画ではなく、スーパー楕円を構成するドットの配列を作る例のため、今回の用途にはアンマッチです。

ただ、同じ記事の後半でFigmaやSketchなどのツールで円形からアンカーポイントを移動させてスーパー楕円を作る例が紹介されていて、おそらくこの例のように4つのベジェ曲線を使い、アンカーポイントを調整することでスーパー楕円が作れるだろうということが予想できました。

sample

実装例

ということで、まずはUIBezierPathでスーパー楕円を作ってみます。 引数で渡した四角形(CGRect)に沿って、4つのベジェ曲線を追加しているだけです。

引数kでアンカーポイントの位置(結果としてスーパー楕円の丸み)を調整できるようにしています。

import UIKit

public struct Superellipse {
  public let bezierPath: UIBezierPath

  public init(in rect: CGRect, k: CGFloat) {
    let path = UIBezierPath(ovalIn: rect)

    let handleX: CGFloat = rect.size.width * k / 2
    let handleY: CGFloat = rect.size.height * k / 2

    let left = CGPoint(x: rect.minX, y: rect.midY)
    let top = CGPoint(x: rect.midX, y: rect.minY)
    let right = CGPoint(x: rect.maxX, y: rect.midY)
    let bottom = CGPoint(x: rect.midX, y: rect.maxY)

    path.move(to: left)
    path.addCurve(
      to: top,
      controlPoint1: CGPoint(x: left.x, y: left.y - handleY),
      controlPoint2: CGPoint(x: top.x - handleX, y: top.y)
    )
    path.addCurve(
      to: right,
      controlPoint1: CGPoint(x: top.x + handleX, y: top.y),
      controlPoint2: CGPoint(x: right.x, y: right.y - handleY)
    )
    path.addCurve(
      to: bottom,
      controlPoint1: CGPoint(x: right.x, y: right.y + handleY),
      controlPoint2: CGPoint(x: bottom.x + handleX, y: bottom.y)
    )
    path.addCurve(
      to: left,
      controlPoint1: CGPoint(x: bottom.x - handleX, y: bottom.y),
      controlPoint2: CGPoint(x: left.x, y: left.y + handleY)
    )

    self.bezierPath = path
  }
}

あとは、ここで作ったUIBezierPathでマスクされるUIViewサブクラスを作ってあげるだけです。

import UIKit

@IBDesignable
public final class SuperellipseView: UIView {
  @IBInspectable public var k: CGFloat = 0.75

  public override func layoutSubviews() {
    super.layoutSubviews()

    let path = Superellipse(in: bounds, k: k).bezierPath
    let mask = CAShapeLayer()
    mask.path = path.cgPath
    layer.mask = mask
  }
}

とても簡単ですね!

利用例

実際に利用するのは簡単です。 例えば、スーパー楕円形のサムネイル画像を表示するなら、

let superellipseView = SuperellipseView(frame: CGRect(x: 40, y: 40, width: 100, height: 100))
superellipseView.backgroundColor = .clear
view.addSubview(superellipseView)

let imageView = UIImageView(image: UIImage(named: "mayuge_dog_face"))
imageView.frame = superellipseView.bounds
superellipseView.addSubview(imageView)

こんな感じにSuperellipseViewaddSubviewして、その上にサムネイル画像を設定したUIImageViewaddSubviewするだけです。

image

課題

本当は 上の記事 で紹介されているような n=2.5 とか n=3.5 といった係数をそのまま反映させるものを作りたかったのですが、私の頭で短時間でこれを実現することはできませんでした…

この辺りわかるかた是非ご教示ください!

公開

上で実装してみた SuperellipseView をお手軽に使ってみたいというかたがいらっしゃったら、

https://github.com/tokorom/SwiftSuperellipse

にSwiftPackage化して公開していますのでお試しください!

Related Entries
Latest Entries