13

Is there a way to use an alternative animation for the new fullscreen modal is iOS 14 in SwiftUI?

At present it slides up from the bottom, but I'd like crossdissolve. I've tried a few things but no luck. I'm thinking the new matchedGeometryEffect() modifier might be of use.

Below is the default use of this new feature

struct ContentView: View {

    @State private var isShowing = false

    var body: some View {
        Button {
            isShowing.toggle()
        } label: {
            Text("Show Modal").font(.largeTitle)
        }.fullScreenCover(isPresented: $isShowing) {
            Text("Hello").font(.largeTitle)
        }
    }
}
DogCoffee
  • 19,820
  • 10
  • 87
  • 120
  • I have doubts about this feasibility with this standard `fullScreenCover` it uses different view hierarchy (if you see on View Debug). – Asperi Jul 04 '20 at 09:05
  • @Asperi Yeah - I've given up on it, but wanted to get some opinions on it. Thanks – DogCoffee Jul 04 '20 at 12:53

3 Answers3

3

Right now there is no direct API to do this but you can have a simple hack using ZStack to solve this issue

The bellow code works just fine as an alternate

@State var isPresented = false

var body: some View {
    
    ZStack {
        
        NavigationView {
            VStack {
                
                Button(action: {
                    // trigger modal presentation
                    withAnimation {
                        self.isPresented.toggle()
                    }
                    
                }, label: {
                    Text("Show standard modal")
                })
                
            }.navigationBarTitle("Standard")
        }
        
        ZStack {
            HStack {
                Spacer()
                VStack {
                    
                    HStack {
                        Button(action: {
                            // trigger modal presentation
                            withAnimation {
                                self.isPresented.toggle()
                            }
                            
                        }, label: {
                            Text("Dismiss")
                                .font(.headline)
                                .foregroundColor(.white)
                        })
                        Spacer()
                        
                        Image(systemName: "xmark.circle.fill")
                            .foregroundColor(.white)
                            .onTapGesture {
                                withAnimation {
                                    self.isPresented.toggle()
                                }
                        }
                    }
                    
                    
                        .padding(.top, UIApplication.shared.windows.filter{$0.isKeyWindow}.first?.safeAreaInsets.top)
                    Spacer()
                }
                Spacer()
            }
            
            
            
        }.background(Color.yellow)
            .edgesIgnoringSafeArea(.all)
            .offset(x: 0, y: self.isPresented ? 0 : UIApplication.shared.keyWindow?.frame.height ?? 0)
         
        
    }
    
}
Albi
  • 1,705
  • 1
  • 17
  • 28
  • If you are in a Navigation Stack, following this approach, makes the back button to be above the modal, do you know how to fix that? – CarlosAndres Jul 24 '23 at 17:49
1

Code bellow works, with matchedGeometryEffect() that you are after.

Its harcoded for now, as I still haven't figure out how to pass data from List-> Detail. If you do, please let me know! ; )

import SwiftUI




struct Grid: View {
    
    let namespace: Namespace.ID
    
    var body: some View {
        
        List{
           Image("cover")
                .resizable()
                .frame(width: 50, height: 50)
                .cornerRadius(4)
                .padding()
                .matchedGeometryEffect(id: "animation", in: namespace)
            
            Image("cover2")
                 .resizable()
                 .frame(width: 50, height: 50)
                 .cornerRadius(4)
                 .padding()
                 .matchedGeometryEffect(id: "animation", in: namespace)
        }

   }
}

struct Detail: View {
    
    let namespace: Namespace.ID
    
    var body: some View {
        
            Image("cover")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .cornerRadius(10)
                .padding(40)
                .matchedGeometryEffect(id: "animation", in: namespace)
        
      
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(#colorLiteral(red: 0.234857142, green: 0.043259345, blue: 0.04711621255, alpha: 1)))
    }
}

struct ContentView: View {
    
    @Namespace private var ns
    @State private var showDetails: Bool = false
    
    var body: some View {
        ZStack {
            Spacer()
            if showDetails {
                Detail(namespace: ns)
            }
            else {
                Grid(namespace: ns)
            }
        }
        .onTapGesture {
            withAnimation(.spring()) {
                showDetails.toggle()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Mane Manero
  • 3,086
  • 5
  • 25
  • 47
-3

So to answer your question, let's dive into problem first.

You need to show your view with different transition from default fullscreencover. If yes then here is the solution.

Option 1: straightforward( for beginners)

    struct ContentView: View {
    
    @State private var isShowing = false
    
    var body: some View {
        ZStack {
            if(!isShowing) {
                    Button {
                        withAnimation {
                        
                        isShowing.toggle()
                        }
                    } label: {
                        Text("Show Modal").font(.largeTitle)
                    }
            } else {
                VStack {
                Text("Hello").font(.largeTitle)
                    Button(action: {
                        withAnimation {
                        isShowing.toggle()
                        }
                    }) {
                        Text("Go back")
                    }
                }
                .frame(width: 400, height: 900) //recommended - adjust this frame size according to you dynamically
                .background(Color.red)
                .transition(.slide) // you can use other transition like .move ,etc.
            }
            
        }
    } }

Option 2: Advanced

  1. Add Transition to your view and make this view frame to cover whole screen or the part you want to cover. To know more about transition look here - https://stackoverflow.com/a/62767038 . You can take an idea to create custom transition.

  2. Add this custom transition to your view with condition like below

    struct ContentView: View {
    
    @State private var isShowing = false
    
    var body: some View {
        ZStack {
            if(!isShowing) {
                    Button {
                        withAnimation {
    
                        isShowing.toggle()
                        }
                    } label: {
                        Text("Show Modal").font(.largeTitle)
                    }
            } else {
                VStack {
                Text("Hello").font(.largeTitle)
                    Button(action: {
                        withAnimation {
                        isShowing.toggle()
                        }
                    }) {
                        Text("Go back")
                    }
                }
                .frame(width: 400, height: 900) //recommended - adjust this frame size according to you dynamically
                .background(Color.red)
                .transition(.rotate) //custom transition rotate from the link shared in 1st point. You can use inbuilt transition like .transition(.slide) and many more
            }
    
        }
    } }
    

Here I'm using custom animation .rotate from the link shared above but you can use other inbuilt transition or make your custom transition as per your requirement.

Reed
  • 944
  • 7
  • 15
  • Nice answer on the one side, but on the other hand you did not answer the question for an alternative transition for ".fullScreenCover", that always comes bottom up. – LukeSideWalker Feb 28 '21 at 21:19
  • .fullScreenCover is a SwiftUI modal presentation modifier, not a transition type / style. The OP wanted to use an alternative transition for that presentation, similar to how alternative transitions can be configured for modal presentations in UIKit. – Scott Ahten Jun 12 '23 at 14:30