19

I'm trying to recreate the iOS 11/12 App Store with SwiftUI. Let's imagine the "story" is the view displayed when tapping on the card.

I've done the cards, but the problem I'm having now is how to do the animation done to display the "story".

As I'm not good at explaining, here you have a gif:

Gif 1 Gif 2

I've thought of making the whole card a PresentationLink, but the "story" is displayed as a modal, so it doesn't cover the whole screen and doesn't do the animation I want.

The most similar thing would be NavigationLink, but that then obliges me to add a NavigationView, and the card is displayed like another page.

I actually do not care whether its a PresentationLink or NavigationLink or whatever as long as it does the animation and displays the "story".

Thanks in advance.

My code:

Card.swift

struct Card: View {
    var icon: UIImage = UIImage(named: "flappy")!
    var cardTitle: String = "Welcome to \nCards!"
    var cardSubtitle: String = ""
    var itemTitle: String = "Flappy Bird"
    var itemSubtitle: String = "Flap That!"
    var cardCategory: String = ""
    var textColor: UIColor = UIColor.white
    var background: String = ""
    var titleColor: Color = .black
    var backgroundColor: Color = .white

    var body: some View {
        VStack {
            if background != "" {
                Image(background)
                    .resizable()
                    .frame(width: 380, height: 400)
                    .cornerRadius(20)

            } else {
                RoundedRectangle(cornerRadius: 20)
                    .frame(width: 400, height: 400)
                    .foregroundColor(backgroundColor)
            }

            VStack {
                HStack {
                    VStack(alignment: .leading) {
                        if cardCategory != "" {
                            Text(verbatim: cardCategory.uppercased())
                                .font(.headline)
                                .fontWeight(.heavy)
                                .opacity(0.3)
                                .foregroundColor(titleColor)
                            //.opacity(1)
                        }

                        HStack {
                            Text(verbatim: cardTitle)
                                .font(.largeTitle)
                                .fontWeight(.heavy)
                                .lineLimit(3)
                                .foregroundColor(titleColor)
                        }

                    }

                    Spacer()
                }.offset(y: -390)
                    .padding(.bottom, -390)

                HStack {
                    if cardSubtitle != "" {
                        Text(verbatim: cardSubtitle)
                            .font(.system(size: 17))
                            .foregroundColor(titleColor)
                    }
                    Spacer()
                }
                .offset(y: -50)
                    .padding(.bottom, -50)
            }
            .padding(.leading)


        }.padding(.leading).padding(.trailing)
    }

}

So

Card(cardSubtitle: "Welcome to this library I made :p", cardCategory: "CONNECT", background: "flBackground", titleColor: .white)

displays: Card 1

amodrono
  • 1,900
  • 4
  • 24
  • 45

2 Answers2

10

SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.

One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.

In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.

struct ContentView: View {
    @State var isPresenting = false
    @State var isFullscreen = false
    @State var sourceRect: CGRect? = nil

    var body: some View {
        ZStack {
            GeometryReader { proxy in
                Button(action: {
                    self.isFullscreen = false
                    self.isPresenting = true
                    self.sourceRect = proxy.frame(in: .global)
                }) { ... }
            }

            if isPresenting {
                GeometryReader { proxy in
                    ModalView()
                    .frame(
                        width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil, 
                        height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
                    .position(
                        self.isFullscreen ? proxy.frame(in: .global).center : 
                            self.sourceRect?.center ?? proxy.frame(in: .global).center)
                    .onAppear {
                        withAnimation {
                            self.isFullscreen = true
                        }
                    }
                }
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

extension CGRect {
    var center : CGPoint {
        return CGPoint(x:self.midX, y:self.midY)
    }
}
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
Palle
  • 11,511
  • 2
  • 40
  • 61
  • Thanks! This is exactly what I needed, but when I try to run this, it gives me the following error: `detected '=' on bool, use '=='`, which can be simply be solved, but then it gives me error 2: `Result values in '? :' expression have mismatching types 'Swift.Optional<_>' and 'Swift.Optional<_>'` – amodrono Aug 29 '19 at 13:11
  • Looks like I missed a parenthesis. Should be fixed now. – Palle Aug 29 '19 at 16:58
  • @Palle, sorry for my ignorance. I'm really just starting with SwiftUI (or Swift in general for that matter) and can't understand what `ModalView()` refers to. Would you mind illustrating how that relates to the `CardView`? Also, I assume the content of `{ ... }` would be an instance of the `CardView` as well? – Alexandre Theodoro Oct 06 '19 at 11:29
  • `ModalView` in this case is just a placeholder for the view that you want to present on top of the other view. The `{...}` is the content of the button in the original view, so yes it could contain a card view. – Palle Oct 09 '19 at 07:39
  • it gives me error please help me out give me any source code if you have. – Ravindra_Bhati Jan 14 '20 at 07:02
  • 2
    it gives me error is the least helpful thing that you can say to help me understand your exact problem. Also, I came up with this code for this answer, so this is everything I have. – Palle Jan 14 '20 at 13:48
8

SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.

Link to Official Documentation

Here's a minimal example:

struct SomeView: View {
    @State var isPresented = false
    @Namespace var namespace
 
    var body: some View {
        VStack {
            Button(action: {
                withAnimation {
                    self.isPresented.toggle()
                }
            }) {
                Text("Toggle")
            }

            SomeSourceContainer {
                MatchedView()
                .matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
            }

            if isPresented {
                SomeTargetContainer {
                    MatchedTargetView()
                    .matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
                }
            }
        }
    }
}
Palle
  • 11,511
  • 2
  • 40
  • 61
  • Thanks! I was just going to post an answer saying this thing haha, finally SwiftUI 2 has things like this and we don't have to resort to ugly workarounds. – amodrono Jun 23 '20 at 19:01
  • I really want to see a backport of this API for iOS 13... should be possible. – Jon Willis Aug 06 '20 at 14:25
  • Thanks! Could you expand your example a bit for the noobs out here? – Adrian Ciolea Aug 07 '20 at 06:29
  • 1
    There's a good blog article on HackingWithSwift: https://www.hackingwithswift.com/quick-start/swiftui/how-to-synchronize-animations-from-one-view-to-another-with-matchedgeometryeffect – Palle Aug 07 '20 at 12:19