1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIで画像2枚をインタラクティブに比較できるスライダーを実装してみる

1
Last updated at Posted at 2026-03-08

はじめに

自作の体型管理アプリでも採用している、2枚の画像をスライダーで直感的に比較するUIの実装方法を解説します。

イメージ

slider_gif.gif

※アプリ内の画像は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の .maskDragGesture を組み合わせることで、複雑に見える画像比較UIも結構シンプルに実装することができました。
この実装は、今回のような体型比較だけでなく、写真編集アプリのフィルター前後比較など、幅広いシーンで応用が可能だと思います。

ぜひ、皆さんのアプリでも取り入れてみてください〜

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?