[SwiftUI] View の大きさをスケーリングで変更する

SwiftUI

SwiftUI での View の大きさを スケーリングを使って、調整する方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

View の大きさを調整したい

View の大きさを調整するときに まず浮かぶのは、.frame だと思います。

しかし、.frame は、View に 大きさの提案をするだけで 指定された View が その大きさになることは保証されません。

誰が View の大きさを決定するか

例えば、子 View が、大きさ 200x200 のサイズになろうとしている時に、親 View から、300x300 でと指定されても、子 View の大きさは、200x200 のままです。

親 View からの提案は、あくまで提案で 最終的にサイズを決定するのは、子 View です。

ちなみに、提案した 300x300 の中の どの位置に 200x200 の 子 View を配置するかは、親 View 次第です。

View の大きさ調整の例

Image のケース

SwiftUI の View として用意されている Image には、便利な modifier があります。

.resizable です。この view modifier を指定すると、提案されたサイズにあうように View のサイズを調整してくれます。

調整する時の比率については、.scaleToFit, .scaleToFill, .aspectRatio 等の view modifier で調整することが可能です。

Image 以外では?

一般に使える view modifier として、.scaleEffect という view modifier が用意されています。


func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View

縦横のスケール値と スケール時の中心点を指定することができます。(別条件で設定する scaleEffect も用意されています。)

scaleEffect で良いのでは?

scaleEffect を使うと、Image だけではなく、さまざまな View をスケーリングして調整できるのですが、スケール値を具体的に指定する必要がある点が、ポイントです。

「本来の2倍の大きさで表示」というような要求であれば、ぴったりの view modifier です。以下のように使うことで実現できます。


MyView()
    .scaleEffect(CGSize(width: 2.0, height: 2.0))

しかし、通常の SwiftUI の方法では、子ビューの実際のサイズ(親 View が提案したサイズではなく、子 View が実際に使用するサイズ)を取得することはできないため、
「iPhone の各機種ごとに異なる解像度の画面いっぱいに表示したい」というような要求に対しては、そのまま使うことはできません。


MyView()
    .scaleEffect(CGSize(width: ??, height: ??))

MyView() の サイズは、この時点では決定していませんので、.scaleEffect に数値を与えることができません。

Preference

子 View から、親 View に情報を伝達する方法として、Preference というものが用意されています。

詳細は別 Post で説明する予定ですが、Preference を使用することで、子 View から親 View へ情報を渡すことができます。

# .navigationTitle なども 同様に Preference の仕組みを使っていると考えられます。

自 View のサイズを渡す Preference

以下のような Preference を使用することで、自分のサイズを親 View に渡すことができます。


struct SizePreferenceKey: PreferenceKey {
    typealias Value = [SizePreferenceData]
    static var defaultValue: [SizePreferenceData] = []
    static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

struct SizePreferenceSetter: View {
    let sizeName: String
    var body: some View {
        GeometryReader { geom in
            Color.clear
                .preference(key: SizePreferenceKey.self, value: [SizePreferenceData(name: sizeName, size: geom.size)])
        }
    }
}

自分のサイズを Preference に設定したいと思っているビューに、以下のように設定します。


    MyView()
        .background(SizePreferenceSetter(sizeName: "targetSize"))

上記のようにすることで、Preference が設定されるようになり、Preference に設定された値は 以下のように view modififer を使用して取得することができます。


    .onPreferenceChange(SizePreferenceKey.self, perform: { prefs in
         for pref in prefs {
            if pref.name == "targetSize" {
                // process pref.size 
            }
        }
    })

ビューのサイズを設定して取得して処理する View Modifier

先の Preference は、自分のサイズを取得することにも使用できます。

以下の View Modifier は、自分のサイズを取得して、指定されたサイズにあうように .scaleEffect を使用して View を拡大縮小するものです。
# 以下のコードでは、min(1.0, scale) とすることで、最大スケール値を 1.0 つまり 拡大は行わないようにしています。


struct Scale: ViewModifier {
    let size: CGSize
    
    @State var horizontalScale: CGFloat = 1.0
    @State var verticallScale: CGFloat = 1.0

    public init(_ width: CGFloat = -1,_ height: CGFloat = -1) {
        self.size = CGSize(width: width, height: height)
    }
    
    func body(content: Content) -> some View {
        content
            .background(SizePreferenceSetter(sizeName: "targetSize"))
            .scaleEffect(CGSize(width: min(1.0, horizontalScale), height: min(1.0, verticallScale)))
            .onPreferenceChange(SizePreferenceKey.self, perform: { prefs in
                for pref in prefs {
                    if pref.name == "targetSize" {
                        self.horizontalScale = self.size.width == -1 ? 1.0 : self.size.width / pref.size.width
                        self.verticallScale = self.size.height == -1 ? 1.0 : self.size.height / pref.size.height
                    }
                }
            })
    }
}

調整したい View に対して以下のように modifier として使用します。

以下では、画面サイズに収まるように ビューをスケール表示しています。


    var body: some View {
        MyView()
          .scale(screenWidth * 1.0, screenHeight * 1.0)
    }
    var screenWidth: CGFloat {
        return UIScreen.main.bounds.size.width
    }
    var screenHeight: CGFloat {
        return UIScreen.main.bounds.size.height
    }

使用例:増えてきている画面サイズへの対応

iPhone の最初の頃は、画面サイズは 1サイズで機種依存を考慮する必要がありませんでした。

しかし、最近では iPhoneSE(2nd) = 375x667 から iPhone 12 Pro Max = 428x926 まで、幅広くなってきて 1つのレイアウトで全機種対応することが難しくなってきました。

解像度の大きなスクリーンでは、その画面の大きさを活かして さまざまな情報を表示したくなるのですが、同じレイアウトでは、解像度の小さいスクリーンに収まらないケースが増えてきました。

特に SwiftUI では、子 View のサイズは、最終的に 各 子View が設定するため、制約ベースの時のような調整が難しい気がします。

今回作った .scale のようなものを使用して、スケールしてしまうのも 解決策の1つになります。

まとめ:View の大きさをスケーリングで変更する方法

View の大きさをスケーリングで変更する方法
  • .scaleEffect を使用することでスケーリングすることができる
  • 子 View からの情報は、Preference 経由で取得する必要がある

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です