Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwuiftUI collapse multiline Text

Tags:

swift

swiftui

I'm trying to add an expand/colapse animation on a Text with multiple lines, and I'm having a strange behaviour.

Below is a gif with the issue. I've set slow animations to make it clear.

https://www.dropbox.com/s/sx41g9tfx4hd378/expand-collapse-stack_overflow.gif

I'm animating the height property of the view, and it seems that the Text will convert immediately to one line disregarding the animation period. Here's some code:

struct ContentView: View {

    @State var expanded = false

    var body: some View {
        VStack(spacing: 20) {
            HStack {
                Button(expanded ? "Colapse" : "Expand") {
                    withAnimation {
                        self.expanded.toggle()
                    }
                }
            }
            VStack(spacing: 10) {
                Text(bigText)
                Text(bigText)
            }
            .frame(height: expanded ? .none : 0)
            .clipped()
            .background(Color.red)
            Text("Thist is another text underneath the huge one. ")
                .font(.system(.headline))
                .foregroundColor(.red)
            Spacer()
        }
    }
}

I have tried a lot of other things, and this is currently the closest to the desired output, which is the same as animating a label inside a UIStackView in UIKit.

Is there a way to do this properly? Is this a bug? Normally the problem is from the developer, but I noticed that if I use a DisclosureGroup the animation works when it's expanding, but when it's collapsing it simply has no animation. So this might actually be a limitation of multi line Text?

Thank you very much.

like image 877
Nuno Gonçalves Avatar asked Sep 27 '21 16:09

Nuno Gonçalves


People also ask

How do I use multiline text in SwiftUI texteditor?

Using SwiftUI TextEditor for Multiline Text Input The first version of SwiftUI doesn’t come with a native UI component for multiline text field. For multiline input, you can wrap a UITextView from the UIKit framework and make it available to your SwiftUI project by adopting the UIViewRepresentable protocol.

How to make a multiline text field that grows with the content?

To make a multiline text field that grows with the content, we specify axis: .vertical as an initializer argument. This will make a text field grow dynamically with the content as long as there is enough space. axis: .horizontal will yield the same result as a normal text field. A multiline text field.

What is the onchange () modifier in SwiftUI?

The new version of SwiftUI introduces an onChange () modifier which can be attached to TextEditor or any other view. Let’s say, if you are building a note application using TextEditor and need to display a word count in real time, you can attach the onChange () modifier to TextEditor like this:


Video Answer


1 Answers

Well, the problem is that we animate frame between nil and 0, but to internal Text items only edge values are transferred.

To solve this it should be done two steps:

  1. make height animatable data, so every change to height value passed to content
  2. calculate real max height of text content, because we need concrete values of animatable range.

So here is a demo of approach. Prepared with Xcode 13 / iOS 15

Note: slow animation is activated in Simulator for better visibility

enter image description here

let bigText = """
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.
"""

struct ContentView: View {

    // we need `true` on construction (not visible) to calculate
    // max content height
    @State var expanded = true    // << required initial true !!

    @State private var maxHeight: CGFloat?

    var body: some View {
        VStack(spacing: 20) {
            HStack {
                Button(expanded ? "Colapse" : "Expand") {
                    withAnimation {
                        self.expanded.toggle()
                    }
                }
            }
            VStack(spacing: 10) {
                Text(bigText)
                Text(bigText)
            }
            .background(GeometryReader { // read content height
                Color.clear.preference(key: ViewHeightKey.self,
                                              value: $0.frame(in: .local).size.height)
            })
            .onPreferenceChange(ViewHeightKey.self) {
                if nil == self.maxHeight {
                    self.maxHeight = $0     // << needed once !!
                }
            }
            .modifier(AnimatingFrameHeight(height: expanded ? maxHeight ?? .infinity : 0))
            .clipped()
            .background(Color.red)
            Text("Thist is another text underneath the huge one. ")
                .font(.system(.headline))
                .foregroundColor(.red)
            Spacer()
        }
        .onAppear {
            // this cases instance redraw on first render
            // so initial state will be invisible for user
            expanded = false    // << set if needed here !!
        }
    }
}

struct AnimatingFrameHeight: AnimatableModifier {
    var height: CGFloat = 0

    var animatableData: CGFloat {
        get { height }
        set { height = newValue }
    }

    func body(content: Content) -> some View {
        content.frame(height: height)
    }
}

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

backup

like image 82
Asperi Avatar answered Oct 08 '22 18:10

Asperi