SwiftUI の position・offset で絶対配置と相対移動を行う

SwiftUI の通常のレイアウトはスタックの中で相対的に位置が決まりますが、position と offset を使うとビューを絶対座標や相対的なずらし量で配置できます。CSS でいう absolute positioning に近い考え方で、通常のレイアウトフローから外れた特殊な配置を実現します。

position:親ビューの座標空間で絶対配置する

position は、ビューの中心点を親ビューの座標空間上の指定した点に移動させます。

ZStack {
    Color.gray.opacity(0.1)
    
    Circle()
        .fill(Color.blue)
        .frame(width: 40, height: 40)
        .position(x: 50, y: 50)
    
    Circle()
        .fill(Color.red)
        .frame(width: 40, height: 40)
        .position(x: 150, y: 100)
}
.frame(width: 200, height: 200)

座標 (50, 50) に青い円、(150, 100) に赤い円が配置されます。position を付けたビューは通常のスタックレイアウトから外れ、指定した座標にピンポイントで配置されることになります。

position の注意点

position はビューのサイズ計算に影響を与えます。position を付けると、そのビューは親のサイズ提案をそのまま自分のサイズとして返すようになります。

// position なし → テキストのサイズに収まる
Text("Hello")
    .background(Color.yellow)

// position あり → 親のサイズいっぱいに広がる
Text("Hello")
    .background(Color.yellow)
    .position(x: 100, y: 50)

この挙動は意外に感じるかもしれません。position の前に frame や background を設定し、position の後にはサイズに影響するモディファイアを付けないようにするのがコツです。

offset:現在位置からの相対移動

offset は、ビューの本来の位置から指定した分だけずらして表示します。position と違い、レイアウト上の占有スペースは変わりません。

VStack(spacing: 20) {
    Text("通常の位置")
        .padding()
        .background(Color.blue.opacity(0.2))
    
    Text("offset で移動")
        .padding()
        .background(Color.red.opacity(0.2))
        .offset(x: 30, y: -10)
    
    Text("次の要素")
        .padding()
        .background(Color.green.opacity(0.2))
}

2 番目の Text は右に 30pt、上に 10pt ずれて表示されますが、「次の要素」の位置は変わりません。offset はあくまで「見た目の移動」であり、レイアウトシステム上の位置は元のままです。

position

親の座標空間で絶対位置を指定する。ビューのレイアウトサイズにも影響し、スタックレイアウトから外れる

offset

現在のレイアウト位置からの相対的なずらし。レイアウト上の占有スペースは元の位置のまま変わらない

offset の実用例:バッジの微調整

overlay で配置したバッジの位置を微調整するのに offset がよく使われます。

Image(systemName: "bell.fill")
    .font(.title)
    .overlay(alignment: .topTrailing) {
        Text("3")
            .font(.caption2)
            .fontWeight(.bold)
            .foregroundColor(.white)
            .frame(width: 18, height: 18)
            .background(Color.red)
            .clipShape(Circle())
            .offset(x: 6, y: -6)
    }

overlay の alignment で大まかな位置を決め、offset で細かくずらすという 2 段階のアプローチです。レイアウトへの影響がないため、周囲のビューを押しのけることなく見た目だけを調整できます。

CGSize 版の offset

offset は x と y を個別に指定する方法のほかに、CGSize を渡す方法もあります。

Text("移動")
    .offset(CGSize(width: 20, height: -10))

機能的には同じですが、アニメーションで移動量を State として管理するときには CGSize の方が扱いやすいこともあるでしょう。

position と offset の使い分け

どちらを使うべきか迷ったときは、次の基準で判断できます。

position を選ぶ場面

カスタムグラフの描画やゲーム UI など、座標を直接指定してビューを配置したいとき。通常のスタックレイアウトとは独立した配置が必要な場面に適している。

offset を選ぶ場面

スタックレイアウトの中で、特定のビューだけ少しずらしたいとき。バッジの微調整やアニメーション中の移動など、レイアウト構造を壊さずに見た目だけ変えたい場面で使う。

一般的なアプリ開発では offset の方が圧倒的に使用頻度が高くなります。position は通常のレイアウトフローを壊すため、使い方を誤るとビューの重なりや意図しないサイズ変化を招きます。まず通常のスタックレイアウトで構成し、微調整が必要な部分だけ offset を使うというアプローチが安全です。