Mark
Mark

Reputation: 18204

How can you transition the size of a view back and forth in SwiftUI?

Edit: With help of Asperi I decided to rewrite the description to better clarify the question with easy copy+paste code.

Expected behavior on all tests: The red rectangle will animate it's size from zero to the size of the parent view when the Present button in the top right corner is tapped. When Present is tapped again, the red rectangle will shrink from the size of the parent view to zero.


TEST #1 PROPERTY STATE CHANGE

Actual behavior:

Works as expected.

Code:

struct ContentView: View {  
    @State private var presentRedBox = false  

    var body: some View {  
        NavigationView {  
            GeometryReader { proxy in  
                ZStack {  
                    // ------  
                    Rectangle().fill(Color.red)  
                        .frame(  
                            width: self.presentRedBox ? proxy.size.width : 0.0,  
                            height: self.presentRedBox ? proxy.size.height : 0.0  
                        )  
                    // ------  
                }  
            }.animation(.default)  
            .navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })  
            .navigationBarTitle(Text(""), displayMode: .inline)  
        }  
    }  
}  

TEST #2 ANIMATABLE/VIEW MODIFIER USING PROPERTY STATE CHANGE

Actual behavior:

Works as expected.

Code:

extension AnyTransition {  
    static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {  
        .modifier(  
            active: SizeTransition(size: from),  
            identity: SizeTransition(size: to)  
        )  
    }  
}  

struct SizeTransition: AnimatableModifier {  
    var size: CGSize  

    var animatableData: AnimatablePair<cgfloat, cgfloat=""> {  
        get { AnimatablePair(size.width, size.height) }  
        set {  
            size.width = newValue.first  
            size.height = newValue.second  
        }  
    }  

    func body(content: Content) -> some View {  
        print(size)  
        return content.frame(  
            width: size.width,  
            height: size.height  
        )  
    }  
}  

struct ContentView: View {  
    @State private var presentRedBox = false  

    var body: some View {  
        NavigationView {  
            GeometryReader { proxy in  
                ZStack {  
                    // ------  
                    Rectangle().fill(Color.red)  
                        .modifier(  
                            SizeTransition(  
                                size: self.presentRedBox ? proxy.size : .zero  
                            )  
                        )  
                    // ------  
                }  
            }.animation(.default)  
            .navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })  
            .navigationBarTitle(Text(""), displayMode: .inline)  
        }  
    }  
} 

TEST #3 ANIMATABLE/VIEW MODIFIER WITH TRANSITION

Actual behavior:

The red rectanble will animate in as expected. However(!) it will NOT animate out but disappear immediately, although the log shows the correct values.

Log Animating In

(0.0, 0.0)  
(1.8118343353271484, 3.3873424530029297)  
(7.392631530761719, 13.821006774902344)  
(16.9350643157959, 31.66120719909668)  
(30.5800838470459, 57.17146110534668)  
(48.38059616088867, 90.45067977905273)  
(70.25803184509277, 131.35197257995605)  
(95.95654678344727, 179.39702224731445)  
(124.99998664855957, 233.6956272125244)  
(156.67254066467285, 292.90953254699707)  
(190.03098106384277, 355.27531242370605)  
(223.97296714782715, 418.73206901550293)  
(257.33140754699707, 481.0978488922119)  
(289.00356674194336, 540.3110160827637)  
(318.04700660705566, 594.6096210479736)  
(343.7447319030762, 642.6531944274902)  
(365.6217727661133, 683.5537490844727)  
(383.42189025878906, 716.8322296142578)  
(397.06651496887207, 742.3417453765869)  
(406.60855293273926, 760.1812076568604)  
(412.18856048583984, 770.613395690918)  
(414.0, 774.0)  

Log Animating Out

(413.61268043518066, 773.2758808135986)  
(410.07547760009766, 766.6628494262695)  
(402.6749496459961, 752.8270797729492)  
(391.2381649017334, 731.4452648162842)  
(375.6612854003906, 702.3232727050781)  
(355.94628524780273, 665.4647941589355)  
(332.24832916259766, 621.1599197387695)  
(304.9215717315674, 570.070764541626)  
(274.5523223876953, 513.2934722900391)  
(241.9665470123291, 452.3722400665283)  
(208.19354438781738, 389.231409072876)  
(174.37908554077148, 326.0130729675293)  
(141.67486381530762, 264.870397567749)  
(111.12004852294922, 207.74617767333984)  
(83.55758285522461, 156.21635055541992)  
(59.59075355529785, 111.40880012512207)  
(39.58871841430664, 74.01369094848633)  
(23.71967124938965, 44.34547233581543)  
(11.994667053222656, 22.42481231689453)  
(4.315790176391602, 8.06865119934082)  
(0.5136623382568359, 0.9603252410888672)  
(0.0, 0.0)  

Code:

extension AnyTransition {  
    static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {  
        .modifier(  
            active: SizeTransition(size: from),  
            identity: SizeTransition(size: to)  
        )  
    }  
}  

struct SizeTransition: AnimatableModifier {  
    var size: CGSize  

    var animatableData: AnimatablePair<cgfloat, cgfloat=""> {  
        get { AnimatablePair(size.width, size.height) }  
        set {  
            size.width = newValue.first  
            size.height = newValue.second  
        }  
    }  

    func body(content: Content) -> some View {  
        print(size)  
        return content.frame(  
            width: size.width,  
            height: size.height  
        )  
    }  
}  

struct ContentView: View {  
    @State private var presentRedBox = false  

    var body: some View {  
        NavigationView {  
            GeometryReader { proxy in  
                ZStack {  
                    // ------  
                    if self.presentRedBox {  
                        Rectangle().fill(Color.red)  
                            .transition(  
                                .modifier(  
                                    active: SizeTransition(size: .zero),  
                                    identity: SizeTransition(size: proxy.size)  
                                )  
                            )  
                    }  
                    // ------  
                }  
            }.animation(.default)  
            .navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })  
            .navigationBarTitle(Text(""), displayMode: .inline)  
        }  
    }  
}  

TEST #4 ANIMATABLE/VIEW MODIFIER WITH TRANSITION FOR OPACITY

Expected behavior:

The red rectangle will animate it's opacity from zero (hidden) to one (visible) when the Present button in the top right corner is tapped. When Present is tapped again, the red rectangle will hide from one (visible) to zero (hidden).

Actual behavior:

Works as expected.

Code:

extension AnyTransition {
    static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
        .modifier(
            active: SizeTransition(size: from),
            identity: SizeTransition(size: to)
        )
    }
}

struct SizeTransition: AnimatableModifier {
    var size: CGSize

    var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get { AnimatablePair(size.width, size.height) }
        set {
            size.width = newValue.first
            size.height = newValue.second
        }
    }

    func body(content: Content) -> some View {
        print(size)
        return content.opacity(Double(size.width))
    }
}

struct ContentView: View {
    @State private var presentRedBox = false

    var body: some View {
        NavigationView {
            GeometryReader { proxy in
                ZStack {
                    // ------
                    if self.presentRedBox {
                        Rectangle().fill(Color.red)
                            .transition(
                                .modifier(
                                    active: SizeTransition(size: .zero),
                                    identity: SizeTransition(size: CGSize(width: 1.0, height: 1.0))
                                )
                            )
                    }
                    // ------
                }
            }.animation(.default)
            .navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
            .navigationBarTitle(Text(""), displayMode: .inline)
        }
    }
}

Upvotes: 1

Views: 1928

Answers (1)

Asperi
Asperi

Reputation: 257603

It is not exactly a solution for what you requested, I know, but under some circumstances can be useful, so I decided to post it.

Note: I started with your previous post code, so might be a bit not aligned with this post.

At first proposed alternate - only animation based.

demo1

struct TestReverseTransitions: View {
    @State private var showRedBox = false
    var body: some View {
        VStack {
           Button("Tap") { self.showRedBox.toggle() }
              RedBox()
                 .modifier(SizeAnimation(size: showRedBox ? 
                           CGSize(width: 200.0, height: 200.0) : .zero))
        }.animation(.default)
    }
}

The animatable modifier is used the same from investigation provided below.

Transitions investigation:

Actually I think this might be a bug, but can be a transition engine limitation, because transitions based on effects, but here is just change of physical view frame, while view is in fact already removed. So 50/50... maybe worth reporting feedback to Apple.

Here is why...

demo2

I use animatable modifier to make frame change explicitly via animatable data and as it seen on Demo2 debug log, frame is really animatable on box removed, but appearance is not, however button is moved as it should. Bug? Maybe.

Code for this case:

extension AnyTransition {
    static func size(from: CGSize, to: CGSize) -> AnyTransition {
        AnyTransition.modifier(
            active: SizeAnimation(size: from),
            identity: SizeAnimation(size: to)
        )
    }
}

struct SizeAnimation: AnimatableModifier {
    var size: CGSize
    var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get { AnimatablePair(size.width, size.height) }
        set {
            size.width = newValue.first
            size.height = newValue.second
        }
    }

    func body(content: Content) -> some View {
        //    print(size) // << uncomment for log sizes !!!
        return content.frame(width: size.width, height: size.height)
    }
}

struct TestReverseTransitions: View {
    @State private var showRedBox = false
    var body: some View {
        VStack {
            Button("Tap") { self.showRedBox.toggle() }
            if self.showRedBox {
                RedBox()
                    .transition(
                        .size(from: CGSize.zero, to: CGSize(width: 200.0, height: 200.0))
                    )
            }
        }.animation(.default)
    }
}

struct RedBox: View {
    var body: some View {
        Rectangle().fill(Color.red)
    }
}

Upvotes: 1

Related Questions