Automatically Adjustable View Height Based on Text Height in Swiftui

Automatically adjustable view height based on text height in SwiftUI

Here is a solution based on view preference key. Tested with Xcode 11.4 / iOS 13.4

demo

struct DynamicallyScalingView: View {
@State private var labelHeight = CGFloat.zero // << here !!

var body: some View {
HStack(spacing: 20) {
Image(systemName: "snow")
.font(.system(size: 32))
.padding(20)
.frame(minHeight: labelHeight) // << here !!
.background(Color.red.opacity(0.4))
.cornerRadius(8)

VStack(alignment: .leading, spacing: 8) {
Text("My Title")
.foregroundColor(.white)
.font(.system(size: 13))
.padding(5)
.background(Color.black)
.cornerRadius(8)
Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
.font(.system(size: 13))
}
.background(GeometryReader { // << set right side height
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
.onPreferenceChange(ViewHeightKey.self) { // << read right side height
self.labelHeight = $0 // << here !!
}
.padding(.horizontal)
}
}

struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}

Dynamic Height of Rectangle as from text content in SwiftUI

From what I understood of your question is that:

  1. You have an HStack in which the leftmost view is a Rectangle and the rightmost view is a Text.
  2. You want the Rectangle to be the same height as the Text.

The problem is that the height of the HStack is based on the tallest child view which happens to be the Rectangle but a Rectangle view does not have any intrinsic size like Text and will occupy all the space the parent provides, or if you manually apply a frame.

You set a width of 20 but leave height and so it takes the entire height it can get.

This indicates that we need to set the height of the Rectangle to be same as the dynamic Text but the problem is that we don't know the height upfront.

To solve this:

  1. First we need to know the height of the dynamic Text.
    • For this we will use GeometryReader and access the height value.
  2. The height is in a child view so we need it to notify the parent it's height value.
    • For this we will use PreferenceKey
  3. The parent view should update the Rectangle when it gets to know the Text height
    • A simple @State variable will suffice now


Solution:

struct ContentLengthPreference: PreferenceKey {
static var defaultValue: CGFloat { 0 }

static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

struct ContentView: View {
@State var textHeight: CGFloat = 0 // <-- this

var body: some View {
HStack {
Rectangle()
.frame(width: 20, height: textHeight) // <-- this

Text(String(repeating: "lorem ipsum ", count: 25))
.overlay(
GeometryReader { proxy in
Color
.clear
.preference(key: ContentLengthPreference.self,
value: proxy.size.height) // <-- this
}
)
}
.onPreferenceChange(ContentLengthPreference.self) { value in // <-- this
DispatchQueue.main.async {
self.textHeight = value
}
}
}
}
  1. Create ContentLengthPreference as our PreferenceKey implementation
  2. on Text; Apply overlay containing GeometryReader
  3. overlay will have same height as Text
  4. in GeometryReader, Color.clear is just a filler invisible view
  5. anchorPreference modifier allows us to access and store height
  6. onPreferenceChange modifier on parent HStack can catch the value passed by child view
  7. parent saves the height to a state property textHeight
  8. textHeight can be applied on Rectangle and will update the view when this value updates

Credits: https://www.wooji-juice.com/blog/stupid-swiftui-tricks-equal-sizes.html



Output (including your header + footer views):

Output



EDIT:

If you have multiple of these in a List then you don't need to do anything. Each row will size automatically upto the Text height.

It's free!!!

struct ContentView: View {
var body: some View {
List(0..<20) { _ in
ArticleView()
}
}
}

struct ArticleView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Circle()
.fill(Color.blue)
.frame(width: 15, height: 15)
.overlay(Circle().inset(by: 2).fill(Color.white))

Text("Headline").font(.headline)
}

HStack {
Rectangle().frame(width: 20)

Text(String(repeating: "lorem ipsum ", count: (5...50).randomElement()!))
}

HStack {
Circle()
.fill(Color.orange)
.frame(width: 15, height: 15)
.overlay(Circle().inset(by: 2).fill(Color.white))

Text("Footer").font(.subheadline)
}
}
}
}

Automatic adjust font size in Text() of Swiftui?

Use minimumScaleFactor(_:) https://developer.apple.com/documentation/swiftui/text/minimumscalefactor(_:)

struct ContentView: View {
var body: some View {
VStack{
Text("Test string")
.minimumScaleFactor(0.1) //<--Here
.frame(width: 15, height: 15)
}
}
}

Make a View the same size as another View which has a dynamic size in SwiftUI

As @Asperi pointed out, this solves the problem: stackoverflow.com/a/62451599/12299030

This is how I solved it in this case:

import SwiftUI

struct TestScreen: View {
@State private var imageHeight = CGFloat.zero

var body: some View {
VStack {
HStack {
Image("blue-test-image")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(GeometryReader {
Color.clear.preference(
key: ViewHeightKeyTestScreen.self,
value: $0.frame(in: .local).size.height
)
})
VStack {
Text("Some Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
Text("Other Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
.frame(maxWidth: .infinity, minHeight: self.imageHeight, maxHeight: self.imageHeight)
}
.onPreferenceChange(ViewHeightKeyTestScreen.self) {
self.imageHeight = $0
}
Spacer()
}
.padding()
}
}

struct ViewHeightKeyTestScreen: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}

struct TestScreen_Previews: PreviewProvider {
static var previews: some View {
TestScreen()
}
}

Align heights of HStack members

Wrap your Image in a Text. Since your image is from SF Symbols, SwiftUI will scale it to match the dynamic type size. (I'm not sure how it will scale other images.)

VStack {
let background = RoundedRectangle(cornerRadius: 10)
.foregroundColor(.red)

ForEach(Font.TextStyle.allCases, id: \.self) { style in
HStack {
Text("\(style)" as String)
.padding()
.background(background)

Spacer()

Text(Image(systemName: "checkmark.seal.fill"))
.padding()
.background(background)
}
.font(.system(style))
}
}

screen shot showing all dynamic type sizes

SwiftUI: How to change the TextField height dynamicly to a threshold and then allow scrolling?

Here is a way for this issue:

struct ContentView: View {

@State private var commitDescr: String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

var body: some View {

TextEditorView(string: $commitDescr)

}

}


struct TextEditorView: View {

@Binding var string: String
@State private var textEditorHeight : CGFloat = CGFloat()

var body: some View {

ZStack(alignment: .leading) {

Text(string)
.lineLimit(5)
.foregroundColor(.clear)
.padding(.top, 5.0)
.padding(.bottom, 7.0)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})

TextEditor(text: $string)
.frame(height: textEditorHeight)

}
.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }

}

}


struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}

Dynamic row hight containing TextEditor inside a List in SwiftUI

You can use an invisible Text in a ZStack to make it dynamic.

struct ContentView: View {
@State var text: String = "test"

var body: some View {
List((1...10), id: \.self) { _ in
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
}
}
}

Note that you should consider changing font size and other properties to match the TextEditor

Preview

How to scale text to fit parent view with SwiftUI?

One can use GeometryReader in order to make it also work in landscape mode.

It first checks if the width or the height is smaller and then adjusts the font size according to the smaller of these.

GeometryReader{g in
ZStack {
Circle().strokeBorder(Color.red, lineWidth: 30)
Text("Text")
.font(.system(size: g.size.height > g.size.width ? g.size.width * 0.4: g.size.height * 0.4))
}
}

Sample Image
Sample Image



Related Topics



Leave a reply



Submit