'SwiftUI: How do I "zoom in" to a specific CardView on a tiled interface?
I am trying to build a tiled interface out of CardViews. I want the user to be able to tap on a Card and have that Card expand to take up the available space, essentially "zooming into" the content. The problem is, the Cards only expand from their center, not to the center of the screen. I have tried everything I can think of, animating position, offset, even tried messing with GeometryReader, but I can't get this simple thing to work. It is time to ask the pros.
Here is the code for ContentView:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack() {
HStack(alignment: .center) {
CardView(cardColor: Color.red)
CardView(cardColor: Color.blue)
CardView(cardColor: Color.purple)
}
HStack(alignment: .center) {
CardView(cardColor: Color.green)
CardView(cardColor: Color.gray)
CardView(cardColor: Color.black)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPad Pro (9.7-inch)"))
.previewLayout(PreviewLayout.fixed(width: 2048, height: 1536))
}
}
In this example, there are 2 rows of 3 tiles, but in the final I would like to be able to support up to 9 tiles.
And this is the code for the CardView:
import SwiftUI
struct CardView: View {
@State var show: Bool = false
var cardColor = Color.blue
let message = "Animatable cards with spring animation applied, custom frame and padding. Also uses SFSymbol for icon in the bottom button. Tap button to see the fill stle of this icon."
var body: some View {
VStack() {
Text("Card in SwiftUI")
.foregroundColor(.white)
.font(.largeTitle)
.bold()
.padding(.top, self.show ? 30 : 20)
.padding(.bottom, self.show ? 20 : 0)
.shadow(radius: 2)
Text(self.message)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(.none)
.animation(.spring())
Spacer()
HStack() {
Image(systemName: self.show ? "slash.circle.fill" : "slash.circle")
.foregroundColor(Color.orange)
.font(Font.title.weight(.semibold))
.imageScale(.small)
.shadow(radius: 2)
Text(self.show ? "To Card" : "To Area")
.foregroundColor(Color.yellow)
.font(Font.title.weight(.semibold))
.shadow(radius: 2)
}
.padding(.bottom, self.show ? 20 : 15)
}
.padding()
.padding(.top, 15)
.background(self.cardColor)
.frame(width: self.show ? UIScreen.screenWidth : 290, height: self.show ? UIScreen.screenHeight : 260, alignment: .center)
.cornerRadius(30)
.shadow(color: Color.gray, radius: 5, x: 5, y: 5)
.animation(.spring())
.edgesIgnoringSafeArea(.all)
.onTapGesture {
self.show.toggle()
}
}
}
I'm trying to make this as simple and self contained as possible, but I realize part of my problem may be the scope of the coordinate system I am trying to affect. Help a brotha out!
Solution 1:[1]
I just implemented a pretty hacky solution. I gave my view an array of type [CGPoint] which stores the location of each card, as well as a singular placeholder CGPoint, to return the cards to their original location once they're closed. I also added a scale modifier that listens to my views isActiveCard boolean (enlarging it when active, shrinking it down to list size when not). With this, when a card is tapped on, I call:
withAnimation {
placeholderLocation = cardLocation[i]
cardLocation[i] = CGPoint(x: UIScreen.main.bounds.size.width/2, y: UIScreen.main.bounds.size.width/2)
isActiveCard = true
}
Then all you need to do within the card is have a dismiss or close button that calls
withAnimation {
cardLocation[i] = placeholderLocation
isActiveCard = false
}
Past the code I just showed, further code would only be required to hide or do something else to the other UI elements when the card is open, or to split this logic between two views leveraging an ObservableObject that both views can rely on for state.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Tim B |