SwiftUIでbyClippingを実現する

2020/10/31に公開

これは何

以前書いた、3点リーダーの省略は改行される場合にテキストが中途半端で途切れる問題がありました。
https://zenn.dev/rnishimu22001/articles/53ea41172144a7601942

改行を許可せず、尚且つ3点リーダーを出さないUIKitUILabelにあるbyClippingの設定で使える表示をSwiftUIでもしたいなと思って調べたのでやり方を備忘録として書いていきます。

before After

サンプルコードはこちらです。
https://github.com/rnishimu22001/SwiftUIPlayground/blob/master/SwiftUIPlayground/TextByClippingPlayground.swift

最初の例

以下のようなコードを書いた場合


struct ParentView: View {
    var body: some View {
        VStack(spacing: 30) {
            Text("Hello!! World")
                .lineLimit(1)
        }
    }
}

struct TextByClipping_Previews: PreviewProvider {
    static var previews: some View {
        ParentView()
            .frame(width: 60, height: 20)
	    .background(Color.red)
    }
}

Textの本来の横幅に対して親Viewの幅が足りないので、表示はこのようになります。

こちらの3点リーダーを省略して、親ViewいっぱいにTextを描画するように修正していきます。

手順

Textを本来のサイズで描画する

Textfunc fixedSize(horizontal: Bool, vertical: Bool)をつけることによって、親Viewのサイズに左右されず、子View本来のサイズで描画することができます。
https://developer.apple.com/documentation/swiftui/text/fixedsize(horizontal:vertical:)


struct ParentView: View {
    var body: some View {
        VStack(spacing: 30) {
            Text("Hello!! World")
                .lineLimit(1)
		// fixedSizeを指定して、Text本来のサイズで描画する
                .fixedSize(horizontal: true, vertical: false)
        }
    }
}

この際にhorizontaltrueverticalfalseにすると水平方向のみ、親Viewのサイズの制約がなくなるのでTextが横に伸びるようになります。

親Viewからはみ出た部分を切り取る

親Viewからはみ出た箇所は切り取ってしまうようにします。

GeometryReaderなどで親Viewのサイズを取得することができます。
取得できた親Viewのサイズをもとにfunc frame(width: CGFloat)でTextにサイズを指定します。

この際に親Viewからはみ出た箇所はfunc clipped()で切り取ってしまいます。


struct ParentView: View {
    var body: some View {
        // GometryReaderで親View内の描画可能範囲を取得
        GeometryReader { reader in
            Text("Hello!! World")
                .lineLimit(1)
                .fixedSize(horizontal: true, vertical: false)
		// frameでTextのサイズを指定する
                .frame(width: reader.size.width)
		// 指定サイズをはみ出た箇所を削除
                .clipped()
        }
    }
}

切り取った後の表示はこのようになります。
中央部分が切り取られているので、Textのサイズに合わせて切り取り箇所を変えていきます。

Textを左寄せにする

func frame(width: CGFloat, alignment: Alignment)で左寄せになるように修正します。
alignmentを.leadingに指定すると左寄せになります。


struct ParentView: View {
    var body: some View {
        GeometryReader { reader in
            Text("Hello!! World")
                .lineLimit(1)
                .fixedSize(horizontal: true, vertical: false)
		// alignmentを指定して、左寄せを基準にする
                .frame(width: reader.size.width, alignment: .leading)
                .clipped()
        }
    }
}

Textのサイズに合わせて、Textを中央寄せか左寄せを選択する

func alignmentGuide(_ g: HorizontalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat)でTextのサイズに合わせて、動的に左側のマージンを調整します。
https://developer.apple.com/documentation/swiftui/view/alignmentguide(_:computevalue:)-7g4ky

このメソッドのViewDimensionsから自身のサイズが取得できます。
親ViewとTextのサイズを比較して、Textのサイズが大きければ左寄せ、小さければ中央寄せになるように左マージンを計算します。

この際のマージンの値のプラスマイナスに注意してください。

struct ParentView: View {
    var body: some View {
        GeometryReader { reader in
            Text("Hello!!")
                .lineLimit(1)
                .fixedSize(horizontal: true, vertical: false)
		// clipされる前のサイズからalignmentを計算する
                .alignmentGuide(.leading, computeValue: { dimension in
                    // 親ViewとTextの横幅の差分をとる
                    let difference = reader.size.width - dimension.width
                    if difference > 0 {
                        // 親Viewの方が大きい場合中央寄せ
			// マイナスに注意
                        return -(difference / 2)
                    } else {
                        // Textの方が大きい場合左寄せ
                        return 0
                    }
                })
                .frame(width: reader.size.width, alignment: .leading)
                .clipped()
        }
    }
}

結果、Textの長さによって動的に中央寄せと左寄せが変わるようになりました 👏

はみ出る場合は左寄せ はみ出なければ中央寄せ

Discussion