Setting an equal width to text views in SwiftUI

Let’s take a look on how to set an equal width to multiple views where the width equals to the widest Text() view in SwiftUI. SwiftUI does not have an easy to use view modifier for this at the moment, therefore we’ll add one ourselves. Example use case is displaying two text bubbles with width equaling to the widest bubble.

Creating a content view with text bubbles

The content view contains two TextBubble views with different text. The minimum width for both Text() values are stored in the @State property. The property is updated by custom equalWidth() view modifiers in TextBubble views using bindings. In other words, the view modifier updates the minimum width based on the text in a TextBubble view and uses the calculated value for both TextBubble views.

struct ContentView: View {
@State private var textMinWidth: CGFloat?
var body: some View {
VStack(spacing: 16) {
TextBubble(text: "First", minTextWidth: $textMinWidth)
TextBubble(text: "Second longer", minTextWidth: $textMinWidth)
}
}
}
struct TextBubble: View {
let text: String
let minTextWidth: Binding<CGFloat?>
var body: some View {
Text(text).equalWidth(minTextWidth) // custom view modifier
.foregroundColor(Color.white)
.padding()
.background(Color.blue)
.cornerRadius(8)
}
}
view raw ContentView.swift hosted with ❤ by GitHub
ContentView with equal widths view modifier.
ContentView without equal widths view modifier.

A view modifier applying equal widths

PreferenceKeys in SwiftUI are used for propagating values from child views to ancestor views. This is what we are using here as well: TextBubble views are reading their size using a GeometryReader and then setting the width of the text to our custom EqualWidthPreferenceKey. The background view modifier is used for layering GeometryReader behind the content view which avoids GeometryReader to affect the layout. Transparent color view is only used for producing a value for the preference key which is then set to the binding in the preference key change callback. The frame view modifier reads the value and makes the returned view wider if needed.

struct EqualWidthPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct EqualWidth: ViewModifier {
let width: Binding<CGFloat?>
func body(content: Content) -> some View {
content.frame(width: width.wrappedValue, alignment: .leading)
.background(GeometryReader { proxy in
Color.clear.preference(key: EqualWidthPreferenceKey.self, value: proxy.size.width)
}).onPreferenceChange(EqualWidthPreferenceKey.self) { (value) in
self.width.wrappedValue = max(self.width.wrappedValue ?? 0, value)
}
}
}
extension View {
func equalWidth(_ width: Binding<CGFloat?>) -> some View {
return modifier(EqualWidth(width: width))
}
}

Summary

We created a view modifier which reads a view width and propagates the value to other views using a preference key, a binding, and a @State property.

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s