Clubhouseのスーパー楕円をSwiftUIで作る
 
            近年流行りだした音声SNS, Clubhouse。

デザインの特徴の1つにふわっとした丸みを帯びた楕円のアイコンがあります。
これは「スーパー楕円」と言われているそうです。
丸よりも丸みを感じる!? スーパー楕円の魅力とデザイン | Spinners Inc.
この楕円を@tokoromさんがUIKitで実装してました。
スーパー楕円UIをiOS+Swiftで実装する | Spinners Inc.
この記事を読んでSwiftUIでも作りたくなったので、作ってみます。
実行環境
- Xcode 12.2
- iOS 14.2
実装方針
tokoromさんと同じようにもと記事の「簡易的な描き方(Vector Draw Tool)」で書かれていたアンカーポイントを移動する方法で実装します。

clipShape修飾子
SwiftUIではViewを任意の二次元図形でクリップするときはclipShape修飾子を使います。
引数にShapeプロトコルに準拠した型を指定します。
今回はSuperEllipseShapeという型を作ってスーパー楕円を表現します。
まずは全体像のコードです。
struct SuperEllipse: View {
    var body: some View {
        Image(systemName: "moon")
            .resizable()
            .frame(width: 100, height: 100)
            .background(Color.yellow)
            .clipShape(SuperEllipseShape(rate: 0.75))
    }
}
画像に対して、clipShapeをすることでクリッピングしてます。
SuperEllipseShapeの実装をみていきます。
SuperEllipseShape
実装はこちらです。
struct SuperEllipseShape: Shape {
    let rate: CGFloat
    func path(in rect: CGRect) -> Path {
        let handleX: CGFloat = rect.size.width * rate / 2
        let handleY: CGFloat = rect.size.height * rate / 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)
        var path = Path()
        path.move(to: left) // 
        path.addCurve(to: top,
                      control1: CGPoint(x: left.x, y: left.y - handleY),
                      control2: CGPoint(x: top.x - handleX, y: top.y))
        path.addCurve(
          to: right,
            control1: CGPoint(x: top.x + handleX, y: top.y),
            control2: CGPoint(x: right.x, y: right.y - handleY)
        )
        path.addCurve(
          to: bottom,
            control1: CGPoint(x: right.x, y: right.y + handleY),
            control2: CGPoint(x: bottom.x + handleX, y: bottom.y)
        )
        path.addCurve(
          to: left,
            control1: CGPoint(x: bottom.x - handleX, y: bottom.y),
            control2: CGPoint(x: left.x, y: left.y + handleY)
        )
        return path
    }
}
Shapeプロトコルにはpathメソッドの準拠が必要です。
実装内容はtokoromさんの記事のSuperellipseの実装とほぼほぼ同じです。
handleXとhandleYでベジエ曲線の調整数値を割り出しています。
left,top,right,bottomは矩形の辺のそれぞれ真ん中の座標です。
addCurveメソッドはパスに3次ベジエ曲線を追加するメソッドです。
Endpointの座標とControl point1, 2を使って曲線を作成します。

SwiftUIのaddcurveメソッドのドキュメントよりUIBezierPathのほうが情報が詳しいです。
path.move(to: left)
path.addCurve(to: top,
              control1: CGPoint(x: left.x, y: left.y - handleY),
              control2: CGPoint(x: top.x - handleX, y: top.y))
このように書くことで
leftの座標からtopの座標までの直線でhandleXとhandleY分ずらしたcontrol pointで曲線を作ります。

addCurveを実行したあとは現在位置はto引数の座標、すなわちtopに移動します。
これをtopからright, rightからbottom, bottomからleftにaddCurveメソッドを一周させるとpathの完成です。
実行するとこのようなViewが表示されます。

まとめ
clipShape修飾子とShapeプロトコルを使ってClubhouseのスーパー楕円をSwiftUIで実装してみました。
思ったよりも簡単でびっくりしています。
柔らかな印象のアイコンを作れるのでアプリで取り入れていきたいですね。
コード
本記事のコードは以下のgistに載せています。
https://gist.github.com/SatoTakeshiX/c7ac3d6c82258aae693a997b86036b69
参考文系
- 丸よりも丸みを感じる!? スーパー楕円の魅力とデザイン | Spinners Inc.
- スーパー楕円UIをiOS+Swiftで実装する | Spinners Inc.
- addCurve(to:controlPoint1:controlPoint2:) | Apple Developer Documentation
- Paths vs shapes in SwiftUI - a free Hacking with iOS: SwiftUI Edition tutorial
- How to mask/clip the bottom part of an Image in SwiftUI? - Stack Overflow
宣伝
インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
SwiftUIを学びたい方、ぜひこちらのリンクをチェックしてください!
