はじめに
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] // 左上と、右上のみを角丸にする
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)
}
}
しかし、提供されている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))
UIViewRepresentableとmaskを組み合わせる
それでは上記の2つを組み合わせてみましょう
以下の実装を行っていきます
- 一部のみを角丸にしたUIViewを、UIViewRepresentableを利用してSwiftUIから呼び出せるようにする
- SwiftUIViewのmask Modifierに、1で作成したビューを受け渡す
- 結果的に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の一部を角丸にすることができました
カスタム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が補完されるようになり、呼び出しのコードも簡潔になりました
最終形
.cornerRadius(20, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner])
おわりに
SwiftUIではUIKitでは出来ていた処理が出来ないといったことに度々直面します。
公式で対応してくれるのが一番望ましいですが、それまではこういったワークアラウンドで、SwiftUIらしさを保ったままコーディングできるよう工夫していくしかなさそうです。
よりいい方法などがあれば是非教えてください