LoginSignup
16
10

More than 3 years have passed since last update.

「一部だけ角丸にする」をSwiftUIで実現する

Last updated at Posted at 2020-01-28

はじめに

UIKitではiOS11からUIViewの一部だけを簡単に角丸にすることができるAPIが提供されました。
これをSwiftUIでも同じように利用しようとすると、標準のAPIでは実現できないことがわかります。

本記事では、UIKitと同じようなインターフェースで、
SwiftUIのViewの一部だけを角丸にする方法をご紹介します。

※ 最終的にはこんな感じで使えるようになります

Rectangle()
    .cornerRadius(12, maskedCorners: [.layerMinXMinYCorner, .layerMinXMaxYCorner])

サンプルコードは以下に置いております。
https://github.com/chocoyama/PartlyRoundedCornerView

UIKitでの一部のみ角丸処理

UIKitではCALayerに以下のプロパティが用意されています。

@available(iOS 11.0, *)
open var maskedCorners: CACornerMask

このプロパティをcornerRadiusプロパティを設定した上で利用することで、
ビューの一部の角のみを丸くすることが簡単にできます。

uiView.layer.cornerRadius = 20
uiView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] // 左上と、右上のみを角丸にする

(表示)
UIKitSample

SwiftUIでの角丸処理

では、SwiftUIではどうでしょうか。

SwiftUIには以下のModifierが用意されており、Viewの全ての角を丸くすることは容易にできます。

@inlinable public func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View
struct ContentView: View {
    var body: some View {
        Rectangle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .cornerRadius(20)
    }
}

(表示)
SwiftUISample

しかし、提供されているAPIは上記のものだけであり、
UIKitで利用できた maskedCorners を指定することができないことが問題となります。

UIViewRepresentableを利用したワークアラウンド

では、SwiftUIでも同様のインターフェースでViewの一部の角丸くするにはどうすればよいでしょうか。
今回採用した方法は「UIViewRepresentable を利用し、一部のみ角丸にしたUIViewを利用してSwiftUIのViewにmaskをかける」という方法です。

UIViewRepresentable

UIViewRepresentableは、UIKitで作られたビューをSwiftUIで利用するためのprotocolで、以下のような形で呼び出します。
細かい点については解説しませんが、ここでは makeUIView でUIViewが初期化されることだけ分かっていれば問題ありません。

struct ContentUIView: UIViewRepresentable {
    func makeUIView(context: UIViewRepresentableContext<ContentUIView>) -> UIView {
        let uiView = UIView()
        // initialization
        return uiView
    }

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ContentUIView>) {
    }
}

mask

mask Modifier はSwiftUIのViewが呼び出せるもので、その名の通りViewにマスクをかけることができます。
下記のサンプルコードは角丸の四角形でImageをマスクしています。

Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
    .mask(RoundedRectangle(cornerRadius: 20))

(表示)
maskSample

UIViewRepresentableとmaskを組み合わせる

それでは上記の2つを組み合わせてみましょう

以下の実装を行っていきます
1. 一部のみを角丸にしたUIViewを、UIViewRepresentableを利用してSwiftUIから呼び出せるようにする
2. SwiftUIViewのmask Modifierに、1で作成したビューを受け渡す
3. 結果的にSwiftUIのViewの一部の角のみを丸くできる

1

struct PartlyRoundedCornerView: UIViewRepresentable {
    let cornerRadius: CGFloat
    let maskedCorners: CACornerMask

    func makeUIView(context: UIViewRepresentableContext<PartlyRoundedCornerView>) -> UIView {
        // 引数で受け取った値を利用して、一部の角のみを丸くしたViewを作成する
        let uiView = UIView()
        uiView.layer.cornerRadius = cornerRadius
        uiView.layer.maskedCorners = maskedCorners
        uiView.backgroundColor = .white
        return uiView
    }

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PartlyRoundedCornerView>) {
    }
}

2

Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
   // 角丸処理を施したUIViewをmaskに利用する
    .mask(PartlyRoundedCornerView(cornerRadius: 20,
                                  maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))

3
このようにSwiftUIのViewの一部を角丸にすることができました:tada:
representableAndMask

カスタムModifierを作成して、よりSwiftUIっぽく仕上げる

それでは仕上げです。
現状の実装でもやりたいことは既に実現できていますが、よりSwiftUIっぽい記述ができるようにしてみましょう。
利用するのは カスタムModifier です。

カスタムModifier

SwiftUIでは標準で用意されたModifierとは別に、自前でModifierを定義することも可能です。
具体的には、ViewModifierプロトコルに準拠させたstructを用意し、bodyメソッドを実装してあげることでこれを実現します。
先ほど作成したコードから、マスクをかけている部分をカスタムModifierに切り出します。

struct PartlyRoundedCornerModifier: ViewModifier {
    let cornerRadius: CGFloat
    let maskedCorners: CACornerMask

    func body(content: Content) -> some View {
        content.mask(PartlyRoundedCornerView(cornerRadius: self.cornerRadius,
                                             maskedCorners: self.maskedCorners))}
}

さらに、ViewのmodifierにViewModifierに適合しているインスタンスを受け渡すと、bodyメソッド内の処理が実行されます。
こうすることで mask Modifier の実行がカスタムModifierのbodyメソッド内に隠蔽されました。

Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
    //.mask(PartlyRoundedCornerView(cornerRadius: 20,
    //                              maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))
    .modifier(PartlyRoundedCornerModifier(cornerRadius: 20,
                                          maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))

Extensionの利用

この状態だとまだ切り出したメリットを実感することができませんが、
Viewにextensionを生やすことでスマートな記述にすることが可能です。

extension View {
    func cornerRadius(_ radius: CGFloat, maskedCorners: CACornerMask) -> some View {
        self.modifier(PartlyRoundedCornerModifier(cornerRadius: radius,
                                                  maskedCorners: maskedCorners))
    }
}
Image(systemName: "person")
    .resizable()
    .frame(width: 100, height: 100)
    .background(Color.red)
    //.modifier(PartlyRoundedCornerModifier(cornerRadius: 20,
    //                                      maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner]))
    .cornerRadius(20, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner])

このような実装を加えることで、標準実装されている cornerRadius を記述すると
先ほど実装したmodifierが補完されるようになり、呼び出しのコードも簡潔になりました:tada:

image.png

最終形

.cornerRadius(20, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner])

おわりに

SwiftUIではUIKitでは出来ていた処理が出来ないといったことに度々直面します。
公式で対応してくれるのが一番望ましいですが、それまではこういったワークアラウンドで、SwiftUIらしさを保ったままコーディングできるよう工夫していくしかなさそうです。
よりいい方法などがあれば是非教えてください:pray:

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10