[SwiftUI] Scalable な要素を Text に合わせた高さで表示する

SwiftUI2021

SwiftUI で テキストが使用する高さに合わせて画像を並べて表示する方法を説明します。

環境&対象

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

  • macOS Monterery beta 5
  • Xcode 13 beta5
  • iOS 15 beta

テキストと画像の高さを合わせて表示したい

Image ビューの高さを Text に合わせることを考えます。

ユースケースのイメージは List 中に表示する時に、Text はきちんと見せたいが、画像は、すこし小さめでも構わないというような時です。以下のような表示がゴールです。

Scalable な要素を Text に合わせた高さ表示
Scalable な要素を Text に合わせた高さ表示

テキストと画像のサイズの決まり方

SwiftUI のビュー Text, Image を使うと、それぞれのビューのサイズは以下のように決まります。

  • Text ビュー: 指定された文字列を表示するために必要なサイズ
  • Image ビュー: 指定されたイメージの持つサイズ

以降では、"Hello, world!" を表示する Text ビューと 150x150 サイズの画像を表示する Image ビューを使っていきます。

調整しないまま表示したテキストと画像

特に調整せずに、Text と Image を横に並べてみます。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/09/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            ForEach(0..<3) { _ in 
                HStack {
                    Text("Hello, world!")
                    Image("Image150x150")
                }
            }
        }
        .padding()
    }
}

以下のようなレイアウトになります。

NoAdjustment
NoAdjustment

.resizable 指定した画像の表示

Image には、表示サイズを調整可能と指定する .resizable という View Modifier がありますので、適用してみます。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/09/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            ForEach(0..<3) { _ in
                HStack {
                    Text("Hello, world!")
                    Image("Image150x150")
                        .resizable()
                }
            }
        }
        .padding()
    }
}

以下のような表示になります。横方向には調整しているようですが、縦方向は変化ありません。

resizable
resizable

ちなみに、.scaleToFit を追加指定すると、以下のようになります。

resizableScaleToFit
resizableScaleToFit

resizable 指定することで、Text の右側の余白を幅いっぱいに使おうとして、.scaleToFit 指定することで、その幅に合わせた高さに調整されているという動作になっています。

Text がもっと横幅をとるものであれば、それに応じて 余白が小さくなり、その結果として画像も小さくなりそうです。

ですが、画像の大きさを調整するために、テキストの横幅(文字数)を調整するというのは、本末転倒です。

画像のサイズを指定してみる

Image は、.resizable と .frame を組み合わせることで、外部から表示サイズを指定することができます。

以下では、表示サイズを 100 x 100 に指定しています。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/09/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            ForEach(0..<3) { _ in
                HStack {
                    Text("Hello, world!")
                    Image("Image150x150")
                        .resizable().scaledToFit()
                        .frame(width: 100, height: 100)
                }
            }
        }
        .padding()
    }
}

レイアウトは以下のようになります。

frameWith100x100
frameWith100x100

.resizable と .frame を組み合わせることで、表示サイズの指定はできますが、テキストと高さを合わせることはできていません。

Text は、.frame で表示サイズを調整できない

では、Text も .frame を使用して表示サイズを指定すればよいと思われがちですが、Text は、.frame で指定されたサイズを採用しません。

先ほどの画像の高さ 100 を Text にも指定してみます。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/09/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            ForEach(0..<3) { _ in
                HStack {
                    Text("Hello, world!")
                        .frame(height: 100)
                    Image("Image150x150")
                        .resizable().scaledToFit()
                        .frame(width: 100, height: 100)
                }
            }
        }
        .padding()
    }
}
TextWithFrame
TextWithFrame

比べてみるとわかりますが、Text に .frame を指定せずに、Image に .frame を指定したレイアウトと同じです。

内部的には、Text は、高さ 100 の 見えないフレーム中の .center に配置されています。レイアウト的には、HStack のデフォルトの動作と同じであるため、結果のレイアウトに差がでていません。

ポイントは、Text は、.frame 指定されても、Text 自身の大きさを変更するわけではない という点です。

つまり、Text と Image の表示高さを合わせるのであれば、Image を調整する方が簡単にできるということです。

Image の高さを指定するには、.frame で指定することが必要

Image の高さを指定することは簡単です。 .resizable 指定してから、.frame 指定すると、指定されたサイズに合わせた表示になります。(実際、なっていました)

問題は、合わせる高さがわからない ということです。Text は、自分の高さを表示する時点で確定されますので、事前に外部から与えられているわけではありません。

ですので、Image を高さ指定するための高さを指定することが難しいのです。

Preference を使用する

このような時に便利に使えるのが、Preference です。

Preference を使うことで、子ビューからの情報を親ビューで取得することができます。

親ビューから子ビューに情報を伝達することは簡単ですので、以下のようなビュー構造とデータの流れで、問題を解決できます。

ビュー構成

  • 親ビュー: HStack
  • 子ビュー: Text と Image

高さ指定

  1. Text から Preference を使って、Text 高さを HStack に伝える
  2. HStack は、Text 高さを使って Image の高さを指定する

実装

実装してみると、以下のような表示になります。

Scalable な要素を Text に合わせた高さ表示
Scalable な要素を Text に合わせた高さ表示

以下のような実装になります。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/09/03
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    // (1)
    @State private var textHeight: CGFloat = 10
    var body: some View {
        List {
            ForEach(0..<3) { _ in
                HStack {
                    Text("Hello, world!")
                         // (2)
                        .background(FrameViewRectPreferenceSetter(prefName: "HelloWorldHeight"))
                    Image("Image150x150")
                        .resizable().scaledToFit()
                         // (3)
                        .frame(height: textHeight)
                }
                // (4)
                .onPreferenceChange(FrameViewRectPreferenceKey.self, perform: { prefs in
                    for pref in prefs {
                        if pref.name == "HelloWorldHeight" {
                            self.textHeight = pref.rect.size.height
                        }
                    }
                })
            }
        }
        .padding()
    }
}
コード解説
  1. Text, Image で使用する高さを @State 変数で定義します(View 内部で変更するため @State 定義が必要です)
  2. Text の高さを Preference に設定します
  3. textHeight を Image にも指定します
  4. Preference の変更通知を受けて、textHeight 変数を更新します

まとめ:Scalable な要素を Text に合わせた高さで表示する

Scalable な要素を Text に合わせた高さで表示する
  • Text の高さを Preference で取得する
  • Text 高さを使って、Scalable な要素の高さを設定する

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

コメントを残す

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