web-dev-qa-db-ja.com

SwiftUIビューでの修飾子の順序がビューの外観に影響する

Appleのシリーズの 最初のチュートリアル に従って、SwiftUIアプリケーションでビューを作成および結合する方法を説明しています。
チュートリアルのセクション6のステップ8では、次のコードを挿入する必要があります。

MapView()
    .edgesIgnoringSafeArea(.top)
    .frame(height: 300)

次のUIが生成されます。

ここで、コード内の修飾子の順序を次のように切り替えると気づきました。

MapView()
    .frame(height: 300) // height set first
    .edgesIgnoringSafeArea(.top)

...Hello Worldラベルとマップの間に余分なスペースがあります。

質問

ここで修飾子の順序が重要なのはなぜですか。また、重要な場合はどうすればわかりますか?

10
LinusGeffarth

テキスト着信の壁

修飾子をMapViewを変更するものと考えない方が良いでしょう。代わりに、MapView().edgesIgnoringSafeArea(.top)は、SafeAreaIgnoringViewbodyであり、自身の上部エッジが安全領域の上部エッジにあるかどうかに応じて本体のレイアウトが異なるMapViewを返すものと考えてください。それを実際に行っているので、そのように考える必要があります。

どうして私が本当のことを言っていると確信できるの?このコードをapplication(_:didFinishLaunchingWithOptions:)メソッドにドロップします。

let mapView = MapView()
let safeAreaIgnoringView = mapView.edgesIgnoringSafeArea(.top)
let framedView = safeAreaIgnoringView.frame(height: 300)
print("framedView = \(framedView)")

ここで、mapViewをoption-クリックして、推論された型(単純なMapView)を表示します。

次に、safeAreaIgnoringViewをオプションクリックして、推定された型を確認します。そのタイプは_ModifiedContent<MapView, _SafeAreaIgnoringLayout>です。 _ModifiedContentはSwiftUIの実装の詳細であり、最初のジェネリックパラメーター(Viewという名前)がContentに準拠する場合、Viewに準拠します。この場合、ContentMapViewであるため、この_ModifiedContentViewです。

次に、framedViewをオプションクリックして、推定された型を確認します。そのタイプは_ModifiedContent<_ModifiedContent<MapView, _SafeAreaIgnoringLayout>, _FrameLayout>です。

したがって、型レベルでは、framedViewはコンテンツのタイプがsafeAreaIgnoringViewであるビューであり、safeAreaIgnoringViewはコンテンツのタイプがmapViewであるビューであることがわかります。

しかし、これらは単なるタイプであり、ネストされたタイプの構造は、実行時に実際のデータで表現されない場合がありますよね? (シミュレーターまたはデバイスで)アプリを実行し、printステートメントの出力を確認します。

framedView =
    _ModifiedContent<
        _ModifiedContent<
            MapView,
            _SafeAreaIgnoringLayout
        >,
        _FrameLayout
    >(
        content:
            SwiftUI._ModifiedContent<
                Landmarks.MapView,
                SwiftUI._SafeAreaIgnoringLayout
            >(
                content: Landmarks.MapView(),
                modifier: SwiftUI._SafeAreaIgnoringLayout(
                    edges: SwiftUI.Edge.Set(rawValue: 1)
                )
            ),
        modifier:
            SwiftUI._FrameLayout(
                width: nil,
                height: Optional(300.0),
                alignment: SwiftUI.Alignment(
                    horizontal: SwiftUI.HorizontalAlignment(
                        key: SwiftUI.AlignmentKey(bits: 4484726064)
                    ),
                    vertical: SwiftUI.VerticalAlignment(
                        key: SwiftUI.AlignmentKey(bits: 4484726041)
                    )
                )
            )
    )

Swiftは出力を1行に出力するため、理解が非常に難しくなるため、出力を再フォーマットしました。

とにかく、実際にはframedViewには明らかにcontentプロパティがあり、その値はsafeAreaIgnoringViewの型であり、そのオブジェクトには独自のcontentプロパティがあり、その値はMapViewです。

したがって、Viewに「修飾子」を適用しても、ビューを実際に変更しているわけではありません。 newViewを作成しています。このbody/contentは元のViewです。


修飾子が何をするかを理解したところで(ラッパーViewsを構成します)、これらの2つの修飾子(edgesIgnoringSafeAreasframe)がレイアウトにどのように影響するかについて合理的に推測できます。

ある時点で、SwiftUIはツリーを走査して各ビューのフレームを計算します。トップレベルのContentViewのフレームとして、画面の安全領域から始まります。次に、ContentViewの本体を訪問します。これは(最初のチュートリアルでは)VStackです。 VStackの場合、SwiftUIはVStackのフレームをスタックの子に分割します。これは、3つの_ModifiedContentの後にSpacerが続くスタックです。 SwiftUIは、子を調べて、それぞれに割り当てるスペースを計算します。最初の_ModifiedChild(最終的にMapViewを含む)には_FrameLayout修飾子があり、そのheightは300ポイントであるため、VStackの高さの最初の_ModifiedChildに割り当てられます。

最終的にSwiftUIは、VStackのフレームのどの部分を各子に割り当てるかを判断します。次に、各子供を訪問してフレームを割り当て、子供を配置します。そのため、_ModifiedContent修飾子を使用して_FrameLayoutにアクセスし、そのフレームを、安全領域の上端と一致し、高さが300ポイントの長方形に設定します。

ビューは_ModifiedContentであり、__FrameLayout修飾子のheightが300であるため、SwiftUIは、割り当てられた高さが修飾子に受け入れ可能であることを確認します。そのため、SwiftUIはフレームをさらに変更する必要はありません。

次に、その_ModifiedContentの子を訪問し、修飾子が `_SafeAreaIgnoringLayoutである_ModifiedContentに到着します。安全領域無視ビューのフレームを、親(フレーム設定)ビューと同じフレームに設定します。

次に、SwiftUIは安全領域を無視するビューの子(MapView)のフレームを計算する必要があります。デフォルトでは、子は親と同じフレームを取得します。ただし、この親は_ModifiedContentであり、その修飾子は_SafeAreaIgnoringLayoutであるため、SwiftUIは、子のフレームを調整する必要がある可能性があることを認識しています。修飾子のedges.topに設定されているため、SwiftUIは親のフレームの上端を安全領域の上端と比較します。この場合、それらは一致するので、Swiftは、上の画面の範囲をカバーするように子のフレームを拡張します安全領域の上部このように、子のフレームは親のフレームの外側に拡張されます。

次に、SwiftUIはMapViewにアクセスして、上記で計算されたフレームを割り当てます。これは、安全領域を超えて画面の端まで伸びます。したがって、MapViewの高さは300で、安全領域の上端を超える範囲になります。

安全領域を無視するビューの周りに赤い境界線を、フレーム設定ビューの周りに青い境界線を描画して、これを確認してみましょう。

MapView()
    .edgesIgnoringSafeArea(.top)
    .border(Color.red, width: 2)
    .frame(height: 300)
    .border(Color.blue, width: 1)

screen shot of original tutorial code with added borders

スクリーンショットは、実際には、2つの_ModifiedContentビューのフレームが一致しており、安全領域の外にはみ出していないことを示しています。 (両方の境界線を表示するには、コンテンツを拡大する必要がある場合があります。)


これが、SwiftUIがチュートリアルプロジェクトのコードで機能する方法です。では、提案したようにMapViewの修飾子を入れ替えるとどうなるでしょうか。

SwiftUIがVStackContentView子を訪問するとき、前の例と同様に、スタックの子の間でVStackの垂直範囲を分割する必要があります。

今回は、最初の_ModifiedContent_SafeAreaIgnoringLayout修飾子を持つものです。 SwiftUIは特定の高さがないことを確認しているため、_ModifiedContentの子を探します。これは、_ModifiedContent修飾子を使用した_FrameLayoutです。このビューの高さは300ポイントに固定されているため、SwiftUIは、安全領域を無視する_ModifiedContentの高さが300ポイントであることを認識しています。したがって、SwiftUIは、VStackの範囲の上位300ポイントをスタックの最初の子(安全領域を無視する_ModifiedContent)に付与します。

その後、SwiftUIは最初の子にアクセスして、実際のフレームを割り当て、その子をレイアウトします。したがって、SwiftUIは、セーフエリアを無視する_ModifiedContentのフレームを、セーフエリアの上位300ポイントに正確に設定します。

次に、SwiftUIは、セーフエリアを無視する_ModifiedContentの子のフレームを計算する必要があります。これは、フレーム設定_ModifiedContentです。通常、子は親と同じフレームを取得します。ただし、親は_ModifiedContentの修飾子を持つ_SafeAreaIgnoringLayoutであり、edges.topであるので、SwiftUIは親のフレームの上端を安全領域の上端と比較します。この例では一致しているため、SwiftUIは子のフレームを画面の上端まで拡張します。したがって、フレームは300ポイントに加え、安全領域の上部の範囲になります。

SwiftUIが子のフレームを設定しようとすると、子が_ModifiedContentであり、修飾子が_FrameLayoutで、heightが300であることがわかります。フレームの高さが300ポイントを超えているため、 tモディファイアと互換性があるため、SwiftUIは強制的にフレームを調整します。フレームの高さを300に変更しますが、と同じフレームになりません。余分な範囲(安全領域の外側)がフレームの上部に追加されましたが、フレームの高さを変更すると、フレームの下部エッジが変更されます。

したがって、最終的な効果は、フレームが、安全領域の上の範囲だけ拡大されるのではなく、移動されることです。フレーム設定_ModifiedContentは、セーフエリアの上部300ポイントではなく、画面の上部300ポイントをカバーするフレームを取得します。

次に、SwiftUIはフレーム設定ビューの子であるMapViewにアクセスし、同じフレームを割り当てます。

これは、同じボーダー描画テクニックを使用して確認できます。

if false {
    // Original tutorial modifier order
    MapView()
        .edgesIgnoringSafeArea(.top)
        .border(Color.red, width: 2)
        .frame(height: 300)
        .border(Color.blue, width: 1)
} else {
    // LinusGeffarth's reversed modifier order
    MapView()
        .frame(height: 300)
        .border(Color.red, width: 2)
        .edgesIgnoringSafeArea(.top)
        .border(Color.blue, width: 1)
}

screen shot of modified tutorial code with added borders

ここで、セーフエリアを無視する_ModifiedContent(今回は青い境界線)が元のコードと同じフレームを持っていることがわかります。これは、セーフエリアの上部から始まります。しかし、フレーム設定_ModifiedContentのフレーム(今回は赤い境界線)が、セーフエリアの上部のエッジではなく、画面の上部のエッジから始まり、下部のエッジがフレームも同じだけシフトアップされています。

25
rob mayoff

はい。します。 SwiftUI Essentialsセッションで、Appleはこれをできるだけ簡単に説明しようとしました。

enter image description here

順序を変更した後-

enter image description here

11
SMP

これらの修飾子は、ビューを変換する関数と考えてください。そのチュートリアルから:

SwiftUIビューをカスタマイズするには、修飾子と呼ばれるメソッドを呼び出します。修飾子はビューをラップして、その表示または他のプロパティを変更します。各モディファイヤは新しいビューを返すため、複数のモディファイヤを縦に重ねてチェーンするのが一般的です。

順序が重要であることは理にかなっています。

次の結果はどうなりますか?

  1. 紙を取る
  2. エッジの周りに境界線を描きます
  3. 円を切り取る

対:

  1. 紙を取る
  2. 円を切り取る
  3. エッジの周りに境界線を描きます
4
Tieme