はじめに
自作の体型管理アプリでも採用している、2枚の画像をスライダーで直感的に比較するUIの実装方法を解説します。
イメージ
※アプリ内の画像はAIによって生成されたイメージ画像です。実在の人物とは一切関係ありません。
このスライダーUIを実際に組み込んでいる、写真比較に特化した体重管理アプリを公開しています。
実装詳細
2枚の画像を重ねて切り抜く(maskの活用)
ZStack で Before(背面) と After(前面) の画像を重ねます。
mask で指定したViewの『不透明な部分(Rectangle)』だけを表示し、『透明な部分(Color.clear)』を切り抜きます。
つまり、Rectangleの部分のみAfterImageが表示され、それ以外の部分(Color.clear)は背面にあるBeforeImageが透けて見えるようになります。
sliderOffset は切り抜く領域を割合(0.0~1.0)で保持して、インタラクティブに制御します。(詳細は後ほど解説します)
@State private var sliderOffset: CGFloat = 0.5
GeometryReader { geometry in
ZStack(alignment: .leading) {
Image("BeforeImage")
Image("AfterImage")
.mask(
HStack(spacing: 0) {
Rectangle().frame(width: geometry.size.width * sliderOffset)
Color.clear
}
)
}
}
「仕切り線」の描画
Rectangle で仕切り線を追加して、幅や見た目を調整します。
@State private var sliderOffset: CGFloat = 0.5
GeometryReader { geometry in
ZStack(alignment: .leading) {
Image("BeforeImage")
Image("AfterImage")
.mask(
HStack(spacing: 0) {
Rectangle().frame(width: geometry.size.width * sliderOffset)
Color.clear
}
)
+ Rectangle()
+ .fill(Color.white)
+ .frame(width: 2, height: geometry.size.height)
+ .overlay(
+ Circle()
+ .fill(.white)
+ .frame(width: 34, height: 34)
+ .overlay(
+ Image(systemName: "arrow.left.and.right")
+ .font(.caption)
+ .foregroundColor(.black)
+ )
+ )
}
}
ジェスチャーによるインタラクティブな操作(DragGesture)
実装の追加
@State private var sliderOffset: CGFloat = 0.5
GeometryReader { geometry in
ZStack(alignment: .leading) {
Image("BeforeImage")
Image("AfterImage")
.mask(
HStack(spacing: 0) {
Rectangle().frame(width: geometry.size.width * sliderOffset)
Color.clear
}
)
Rectangle()
.fill(Color.white)
.frame(width: 2, height: geometry.size.height)
.overlay(
Circle()
.fill(.white)
.frame(width: 34, height: 34)
.overlay(
Image(systemName: "arrow.left.and.right")
.font(.caption)
.foregroundColor(.black)
)
)
+ .offset(x: geometry.size.width * sliderOffset - 1, y: 0)
+ .gesture(
+ DragGesture().onChanged { value in
+ sliderOffset = min(max(value.location.x / geometry.size.width, 0), 1)
+ }
+ )
}
}
offset による位置の同期
offsetのx軸の値はgeometry.size.width * sliderOffset - 1にしています。
これはmaskしているRectangleのframeに合わせます。
-1 は線の太さ(2px)の半分だけ左にずらして、ちょうど中心に合わせます。 これを行わないと、線の描画中心が境界線にくるため、見た目上1pxだけ右にズレてしまいます。
DragGesture による座標の正規化
ユーザーがスライダーを指で動かした時の処理です。
- 割合への変換
- 指の位置(
value.location.x)を全体の幅(geometry.size.width)で割ることで、ピクセル単位の数値を 0.0 〜 1.0 の割合に変換します
- 指の位置(
- 範囲の制限
-
min(max(..., 0), 1)を使うことで、指が画像の左右に飛び出しても、値が 0 未満や 1 超えにならないよう安全にガードしています
-
これによってmaskされている下の画像と上の画像の表示割合を制御します。
最後に
SwiftUIの .mask と DragGesture を組み合わせることで、複雑に見える画像比較UIも結構シンプルに実装することができました。
この実装は、今回のような体型比較だけでなく、写真編集アプリのフィルター前後比較など、幅広いシーンで応用が可能だと思います。
ぜひ、皆さんのアプリでも取り入れてみてください〜
