zumzum
zumzum

Reputation: 20158

SwiftUI: presenting modal view with custom transition from frame to frame?

I am trying to replicate what I used to do in UIKit when presenting a ViewController using UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning.

So, for example, from a view that looks like this:

enter image description here

I want to present a view (I would say a modal view but I am not sure if that is the correct way to go about it in SwiftUI) that grows from the view A into this:

enter image description here

So, I need view B to fade in growing from a frame matching view A into almost full screen. The idea is the user taps on A as if it wanted to expand it into its details (view B).

I looked into SwiftUI transitions, things like this:

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}

So, I think I need to build a custom transition. But, I am not sure how to go about it yet being new to this.

How would I build a transition to handle the case as described? Being able to have a from frame and a to frame...?

Is this the right way of thinking about it in SwiftUI?

New information:

I have tested matchedGeometryEffect.

Example:

struct TestParentView: View {
    
    
    @State private var expand = false
    @Namespace private var shapeTransition
    
    var body: some View {

        VStack {
            if expand {
         
                // Rounded Rectangle
                Spacer()
         
                RoundedRectangle(cornerRadius: 50.0)
                    .matchedGeometryEffect(id: "circle", in: shapeTransition)
                    .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
                    .padding()
                    .foregroundColor(Color(.systemGreen))
                    .animation(.easeIn)
                    .onTapGesture {
                        expand.toggle()
                    }
         
            } else {
         
                // Circle
                RoundedRectangle(cornerRadius: 50.0)
                    .matchedGeometryEffect(id: "circle", in: shapeTransition)
                    .frame(width: 100, height: 100)
                    .foregroundColor(Color(.systemOrange))
                    .animation(.easeIn)
                    .onTapGesture {
                        expand.toggle()
                    }
         
                Spacer()
            }
        }
    }

}

It looks like matchedGeometryEffect could be the tool for the job. However, even when using matchedGeometryEffect, I still can't solve these two things:

  1. how do I include a fade in / fade out animation?
  2. looking at the behavior of matchedGeometryEffect, when I "close" view B, view B disappears immediately and what we see animating is view A from where B was back to view A's original frame. I actually want view B to scale down to where A is as it fades out.

Upvotes: 2

Views: 2176

Answers (1)

Hackinator
Hackinator

Reputation: 162

You would have to use the .matchedGeometryEffect modifier on the two Views that you would like to transition.

Here is an example:

struct MatchedGeometryEffect: View {
    
    @Namespace var nspace
    @State private var toggle: Bool = false
    
    var body: some View {
        HStack {
            if toggle {
                VStack {
                    Rectangle()
                        .foregroundColor(Color.green)
                        .matchedGeometryEffect(id: "animation", in: nspace)
                        .frame(width: 300, height: 300)
                    
                    Spacer()
                }
                
            }
            
            if !toggle {
                VStack {
                    
                    Spacer()
                    
                    Rectangle()
                        .foregroundColor(Color.blue)
                        .matchedGeometryEffect(id: "animation", in: nspace)
                        .frame(width: 50, height: 50)
                }
            }
        }
        .padding()
        .overlay(
        
            
            Button("Switch") { withAnimation(.easeIn(duration: 2)) { toggle.toggle() } }
            
        
        )
    }
}

enter image description here

Image should be a GIF

The main two parts of using this modifier are the id and the namespace.

The id of the two Views you are trying to match have to be the same. They then also have to be in the same namespace. The namespace is declared at the top using the @Namespace property wrapper. In my example I used "animation", but it can really be anything, preferably something that can uniquely identify the Views from other types of animations.

Another important piece of information is that the '''@State''' variable controlling the showing/hiding of Views is animated. This is done through the use of withAnimation { toggle.toggle() }.

I'm also quite new to this, so for some more information you can read this article I found from the Swift-UI Lab:

https://swiftui-lab.com/matchedgeometryeffect-part1/

Upvotes: 1

Related Questions