はじめに
SwiftUI で Image を表示する際、見た目を工夫したいことはよくある。丸いプロフィール画像、角丸カード、複雑なロゴ形状...こうした場合に活躍するのが clipShape modifier だ。
この記事では、clipShape を使った 5 つの実装パターン、mask との使い分け、iOS 18 での新機能、実装時の注意点まで、実践的に解説する。
clipShape とは何か
基本的な役割
clipShape は、View を指定した Shape の形状でクリッピングする modifier。Image だけでなく、あらゆる View に適用できる。iOS 13.0 以降で使用可能。
使い方はシンプル。View に .clipShape() を追加して、括弧の中に使いたい Shape を指定するだけ。
clipShape と mask:何が違うのか
同じクリッピングに見えるが、内部動作は異なる。clipShape は Shape のアウトラインを使ったシンプルなクリッピング。mask はピクセルレベルで自由に制御できる。
使い分けの指針は明確だ。形状がシンプルなら clipShape。複雑な形状やグラデーション、半透明を含むなら mask。
| 項目 | clipShape | mask |
|---|---|---|
| 用途 | Shape ベースのクリップ | ピクセルレベル制御 |
| パフォーマンス | 高速(推奨) | 遅い |
| 扱える形状 | Circle, Rectangle 等 | 任意(グラデーション等) |
| 一般的な選択 | 9 割はこちら | 特殊な要件時のみ |
よく使う 5 つのパターン
1. 円形クリップ(プロフィール画像)
最も一般的。プロフィール画像、アバター、丸いボタン...こうした場面では Circle を使う。
import SwiftUI
struct ProfileView: View {
var body: some View {
Image("profileImage")
.resizable()
.scaledToFill()
.frame(width: 120, height: 120)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.blue, lineWidth: 2)
)
}
}
ポイント:resizable() と scaledToFill() でアスペクト比を保ったまま枠を埋める。overlay で枠線を追加すると、さらに見栄えが良くなる。
2. 角丸四角形(カード、サムネイル)
RoundedRectangle でコーナーを丸くする。cornerRadius で丸さを調整。カード、サムネイル、バナーに使う。
struct CardView: View {
var body: some View {
Image("thumbnail")
.resizable()
.scaledToFill()
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 4)
}
}
cornerRadius は 8-16 が標準的。大きすぎるとピルになってしまう。shadow() を組み合わせると、カードとして浮き上がって見える。
3. 楕円形(ロゴ、複雑な画像)
横長や縦長の楕円が必要な場合、Ellipse を使う。frame のアスペクト比に応じて形状が決まる。
struct LogoView: View {
var body: some View {
Image("logo")
.resizable()
.scaledToFill()
.frame(width: 200, height: 100)
.clipShape(Ellipse())
}
}
4. カスタム形状(Path でオリジナル図形)
五角形、三角形、ハート...こうした複雑な形状は、Shape プロトコルに準拠したカスタムクラスで定義する。
struct StarClipView: View {
var body: some View {
Image("icon")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.clipShape(StarShape())
}
}
// 五角形を定義
struct StarShape: Shape {
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
var path = Path()
for i in 0..<5 {
let angle = CGFloat(i) * 0.8 * .pi - .pi / 2
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
if i == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
path.closeSubpath()
return path
}
}
Shape プロトコルには path(in:) メソッドを実装する。CGPath を使ってフリーハンドで図形を描く。
5. アニメーション付きクリップ
状態に応じて形状を変える。@State で isExpanded を持って、tap で切り替える。形状を動的に変える場合は AnyShape でラップする。
struct ExpandableClipView: View {
@State private var isExpanded = false
var body: some View {
VStack {
Image("icon")
.resizable()
.scaledToFill()
.frame(
width: isExpanded ? 250 : 150,
height: isExpanded ? 250 : 150
)
.clipShape(
isExpanded ?
AnyShape(RoundedRectangle(cornerRadius: 32)) :
AnyShape(Circle())
)
.onTapGesture {
withAnimation(.spring()) {
isExpanded.toggle()
}
}
Text(isExpanded ? "Collapse" : "Expand")
.font(.caption)
.foregroundColor(.gray)
}
.padding()
}
}
// 複数の Shape を一つの型で扱うためのラッパー
struct AnyShape: Shape {
private let closure: (CGRect) -> Path
init(_ shape: S) {
self.closure = { shape.path(in: $0) }
}
func path(in rect: CGRect) -> Path {
closure(rect)
}
}
Tap でアニメーション付きで形状が変わる。withAnimation(.spring()) で自然な動きになる。
iOS 18 での新機能と対応
ResizableShape
iOS 18 では、一部の Shape がリサイズに対応した。従来は Path でカスタマイズが必要だった部分が、より簡潔に書ける。
ただし、iOS 17 以前との互換性を保つなら、条件分岐で対応版と非対応版を分ける必要がある。
互換性の保ち方
@available(iOS 18, *)
struct iOS18View: View {
var body: some View {
Image("icon")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// iOS 17 以前向け
@available(iOS, introduced: 13.0, deprecated: 18.0)
struct LegacyView: View {
var body: some View {
Image("icon")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
実装時の注意点
resizable() は必須
Image のデフォルトサイズは固定。clipShape を使う前に .resizable() を必ず呼ぶ。さもないと期待サイズに拡大されない。
frame は clipShape の前
順序が大事。frame を先に指定して、その後に clipShape を適用する。逆順だと期待通りにクリップされない。
// ✓ 正しい順序
Image("icon")
.resizable()
.frame(width: 150, height: 150)
.clipShape(Circle())
// ✗ 間違い
Image("icon")
.clipShape(Circle())
.frame(width: 150, height: 150)
.resizable()
高い解像度では shadow を避ける
clipShape + shadow は、複数の render pass が必要になり、パフォーマンスが低下する場合がある。本当に必要でない限り、shadow は避ける。
複雑な Path は計算量に注意
カスタム Path を定義する場合、複雑な計算は避ける。View の再描画のたびに path() が呼ばれるため、重い計算は実行時パフォーマンスに影響する。
よくある間違いと対処
「クリップが効かない」
原因は、大体 .resizable() が漏れているか、frame が指定されていない。Image のデフォルトサイズで試しているケースが多い。
「アニメーション中に形状がちらつく」
clipShape アニメーションの制限。見栄えが重要なら、mask で代替えすること。
「カスタム Shape がうまく動かない」
Shape の path() メソッドが rect に対応しきれていない。デバッグは、異なるサイズで試して、形状が正しく拡大縮小されるか確認。
関連記事
まとめ
clipShape で Image をカスタム形状にクリップするのは、SwiftUI では基本的なテクニック。使い分けは単純。
シンプルな形(丸、角丸)なら clipShape。複雑な形やグラデーションマスクなら mask。この基準で判断すれば、9 割のケースは解決する。
5 つのパターンを把握していれば、ほぼすべての画像クリッピング要件に対応できるはずだ。