1

I am building a card game (Set) and when dealing the cards I am currently using

withAnimation{} and .transition(.move(edge: .bottom)

to have the cards animate from the bottom edge of the screen when the user taps the deal button. I want to make it so that the cards 'fly out' of the button and was wondering how I find the location of the button.

Once I find the position of the button I intend to use .offset to have them 'fly' out. Is there a better way?

I know it is possible using geometry reader, but I have not found out how to do so.

Here is my current code

struct SetGameView: View {
    
    @ObservedObject var viewModel: SetViewModel = SetViewModel()
    var location: (CGFloat, CGFloat) = (0.0, 0.0)   

    var body: some View {
        VStack {
            HStack {
                Label("Score: \(viewModel.score)", systemImage: "face.dashed").font(.title).padding(.leading)
                Spacer()
            }
            Grid(viewModel.dealtCards) { card in
                CardView(card: card)
                    .transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .leading)))
                    .padding()
                    .onTapGesture { withAnimation { viewModel.choose(card) } }
            } .onAppear { withAnimation(.easeInOut) { deal(12) } }
            Divider()
            HStack {
                CreateNewGameButton(viewModel: viewModel)
                DealNewCardbutton(viewModel: viewModel, deal: deal)
            }.padding(.horizontal).frame(maxHeight: 50)
        }
    }
    
    struct DealNewCardbutton: View {
        
        var viewModel: SetViewModel
        let deal: (Int) -> Void
        
        init(viewModel: SetViewModel, deal: @escaping (Int) -> Void) {
            self.viewModel = viewModel
            self.deal = deal
        }
        
        var body: some View {
            GeometryReader { geo in
                Button(action: {
                    deal(3)
                }){
                    ZStack {
                        RoundedRectangle(cornerRadius: 10.0).foregroundColor(.blue)
                        Text("Deal Three Cards").foregroundColor(.white)
                    }
                }.onAppear {
                    print(geo.frame(in: .global).midX, geo.frame(in: .global).midY)
                }
            }
        }
    }

Heres a video of how it currently works.

https://i.imgur.com/NJfOjBP.mp4

I want the cards to all 'fly out' from the deal button.

Thomas Braun
  • 1,109
  • 2
  • 13
  • 27

1 Answers1

0

Ok. This is pretty tricky. You can get the position of the button via GeometryGetter, but I'd recommend using anchorPreference. The only downside is that you'll have to specify your grid in an overlay or background of the view containing the button.

import SwiftUI

struct BoundsPreferenceKey: PreferenceKey {

    static var defaultValue: [Int: Anchor<CGRect>] = [:]
    
    static func reduce(value: inout [Int: Anchor<CGRect>], nextValue: () -> [Int: Anchor<CGRect>]) {
        value.merge(nextValue()) { v1, _ in v1 }
    }
}

extension View {
    func reportBounds(id: Int) -> some View {
        self.anchorPreference(key: BoundsPreferenceKey.self, value: .bounds, transform: { [id: $0] })
    }
}

private extension Int {
    static let dealButtonId = 0
}

var body: some View {
    VStack {
        Divider()
        HStack {
            CreateNewGameButton(viewModel: viewModel)
            DealNewCardbutton(viewModel: viewModel, deal: deal)
                .reportBounds(id: .dealButtonId)
        }   .padding(.horizontal).frame(maxHeight: 50)
    }   .overlayPreferenceValue(BoundsPreferenceKey.self) { preferences in
        GeometryReader { geometry in
            self.getOverlay(preferences: preferences, geometry: geometry)           
        }
    }
}

func getOverlay(preferences:(preferences : [Int: Anchor<CGRect>], geometry: GeometryProxy) -> some View {
        
    var center: CGPoint = .zero

    if let anchor = preferences[.dealButtonId] {
        let rect = geometry[anchor]
        center = CGPoint(x: rect.midX, y: rect.midY)
    }
    
    return VStack {
        HStack {
            Label("Score: \(viewModel.score)", systemImage: "face.dashed").font(.title).padding(.leading)
            Spacer()
        }
        Grid(viewModel.dealtCards) { card in
            CardView(card: card)
                // USE CENTER here in your transition and/or offset
                .transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .leading)))
                .padding()
                .onTapGesture { withAnimation { viewModel.choose(card) } }
        } .onAppear { withAnimation(.easeInOut) { deal(12) } }
        Spacer()
    }
}

Note that I moved the grid and other views to the overlay, you may need to set explicit frame heights (to the spacer) to make sure the bottom of the grid and the top of the button align. I left the actual implementation of the transition for you.

Jack Goossen
  • 799
  • 4
  • 14