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

SwiftUI

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

環境&対象

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

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

View の大きさを調整したい

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

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

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

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

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

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

View の大きさ調整の例

Image のケース

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

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

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

つまり、大きさを変更したい View が Image である時は、.resizable をつけることで、親 View から渡された大きさに調整して表示されます。

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) = 375×667 から iPhone 12 Pro Max = 428×926 まで、幅広くなってきて 1つのレイアウトで全機種対応することが難しくなってきました。

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

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

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

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

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

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

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

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