ButtonAction overwrites preceding ButtonActions - swift

I have an Imageview that has six buttons on top of it. Depending on which button is being clicked, I want to change the image. However, somehow the ButtonActions don't work as expected and I wasn't able to figure out how to change the behavior correctly.
The ZStack for the Overlay looks like that:
class ImageName: ObservableObject {
#Published var name: String = "muscleGuy"
}
struct MuscleGuy: View {
#ObservedObject var imageName = ImageName()
var body: some View {
VStack(alignment: .center) {
Spacer()
ZStack(alignment: .center) {
GeometryReader { geometry in
//the buttons, overlaying the image
ButtonActions( imageName: self.imageName).zIndex(10)
//the image
ImageView(imageName: self.imageName, size: geometry.size).zIndex(2)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
}
}
}
}
And to aggregate the different buttons, I created the ButtonActions-struct:
struct ButtonActions: View {
#State var imageName: ImageName
var body: some View {
VStack{
GeometryReader { geometry in
HStack {
Button(action: {
self.imageName.name = "arms_chest"
print(self.imageName.name)
}) {
armsRectangle(size: geometry.size)
}
Button(action: {
self.imageName.name = "abs_lower_back"
print(self.imageName.name)
}) {
absRectangle(size: geometry.size)
}
}
Button(action: {
self.imageName.name = "legs"
print(self.imageName.name)
}) {
legRectangle(size: geometry.size)
}
}
}
}
}
The legRectangle contains one rectangle only, whereas the armsRectangle contains three Rectangles and the absRectangle contains two. They are always nested in a HStack.
I don't want to overload my question with code, so here is one example Rectangle:
struct absRectangle: View {
var size: CGSize
var body: some View {
HStack {
Rectangle().foregroundColor(.red)
.frame(width: size.width/8 + 8, height: size.height/9)
.position(x: -size.width/4, y: size.height/2 - 40)
Rectangle().foregroundColor(.red)
.frame(width: size.width/8 + 8, height: size.height/9)
.position(x: -10, y: size.height/2 - 40)
}
}
}
The behavior that occurs is that the legRectangle always takes over the whole screen. So wherever I tab, the legRectangle-action is being executed.
When commenting that section out and assigning the same zIndex to both the armsRectangle and the absRectangle, it recognizes different tap actions. But the behavior isn't correct either. For example only the right ab-rectangle-action is triggered and the left one again also leads to the arms-rectangle-action.
I feel like the behavior has something to do with the alignment within the HStacks/VStacks/ZStack but I can't figure out what exactly is wrong.

Related

SwiftUI translate and rotate view with matchedGeometryEffect strange behavior

I'm trying to simulate dealing cards in SwiftUI. I'm testing with one card, and the goal is to animate the card in the center (image1) to one of the sides (image2). I would like that the animation would rotate and translate the card simultaneously, but with this code the card rotates immediately without animation and then it translates animatedly. Any idea to get the rotation and translation effects simultaneously into the animation?
import SwiftUI
struct Card: Identifiable {
var id: String {
return value
}
let value: String
var dealt: Bool = false
}
struct CardView: View {
let card: Card
var flipped: Bool = false
var body: some View {
ZStack {
Color.white
Text("\(card.value)")
.padding(4)
Color.red.opacity(flipped ? 0.0 : 1.0)
}
.border(.black, width: 2)
}
}
struct CardsTableView: View {
#Namespace private var dealingNamespace
#State var card = Card(value: "1", dealt: false)
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.clear)
.border(.black, width: 2)
.padding(10)
VStack {
ZStack {
centerCard
lateralCard
}
Spacer()
Button {
withAnimation(.linear(duration: 1)) {
card.dealt.toggle()
}
} label: {
Text("Deal")
}
.padding()
}
}
}
var centerCard: some View {
VStack {
Spacer()
if !card.dealt {
CardView(card: card)
.frame(width: 40, height: 70)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity))
}
Spacer()
}
}
var lateralCard: some View {
HStack {
Spacer()
if card.dealt {
CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.frame(width: 40, height: 70)
.rotationEffect(.degrees(-90))
.transition(AnyTransition.asymmetric(insertion: .identity, removal: .opacity))
}
}
.padding(.trailing, 20)
}
}
The matchedGeometryEffect modifier doesn't know about the rotationEffect modifier, so neither view's rotation is animated during the transition. I'll explain how to get the animation you want in two ways: using transitions and using “slots”. Both solutions produce this animation:
Using transitions
You can use a custom .modifier transition to animate the rotation. I wouldn't do it this way, but since it has a similar structure as the code you posted, I'll explain it first.
For the sake of this answer, let's simplify CardView:
struct CardView: View {
var body: some View {
Text("C")
.foregroundColor(.black)
.padding()
.background(Color.white)
.border(Color.black, width: 1)
}
}
To animate rotation, we need a ViewModifier type that applies the rotation effect:
struct CardRotationModifier: ViewModifier {
var angle: Angle
func body(content: Content) -> some View {
content.rotationEffect(angle)
}
}
Here's CardTableView:
struct CardTableView: View {
#Namespace var namespace
#State var isSide = false
var body: some View {
ZStack {
VStack {
if !isSide {
topCard
}
Spacer()
}
HStack {
Spacer()
if isSide {
sideCard
}
}
}
.padding()
.background(Color.mint)
.onTapGesture {
withAnimation(.linear) {
isSide.toggle()
}
}
}
}
And finally here are the top and side card views:
extension CardTableView {
var topCard: some View {
CardView()
.matchedGeometryEffect(id: 0, in: namespace)
.transition(
.modifier(
active: CardRotationModifier(angle: .degrees(90)),
identity: CardRotationModifier(angle: .zero)))
}
var sideCard: some View {
CardView()
.matchedGeometryEffect(id: 0, in: namespace)
.transition(
.modifier(
active: CardRotationModifier(angle: .zero),
identity: CardRotationModifier(angle: .degrees(90))))
}
}
Note that the side card doesn't have a .rotationEffect. Instead, both cards have a transition that applies CardRotationModifier. SwiftUI applies the active modifier at the start of an entrance transition and the end of an exit transition. It applies the identity modifier at the end of an entrance transition, the start of an exit transition, and the entire time the view is “at rest” (present and not transitioning). So the top card normally has rotation zero, and the side card normally has rotation 90°, and each card is animated to the other's rotation during a transition.
What I don't like about this solution is that the transitions are configured specifically for moving a card between the top and side positions. The transition on the top position knows about the rotation of the side position, and vice versa. So what if you want to add a left-side position with a rotation of -90°? You've got a problem. Now you need to dynamically set the transition of each position based on where the card is moving from and to. Every position needs to know details of every other position, so it can be O(N) work to add another position.
Using slots
Instead, I would use what I think of as “slots”: put a hidden view at each possible position (“slot”) of a card. Then, use a view with a persistent identity to draw the card, and tell that persistent view to match the geometry of whichever slot it should occupy.
So, we need a way to identify each slot:
enum Slot: Hashable {
case top
case side
}
Now CardTableView lays out a subview for each slot, and a view for the card:
struct CardTableView: View {
#Namespace var namespace
#State var isSide = false
var body: some View {
ZStack {
topSlot
sideSlot
card
}
.padding()
.background(Color.mint)
.onTapGesture {
withAnimation(.linear) {
isSide.toggle()
}
}
}
}
Here are the slot subviews:
extension CardTableView {
var topSlot: some View {
VStack {
CardView()
.hidden()
.matchedGeometryEffect(id: Slot.top, in: namespace)
Spacer()
}
}
var sideSlot: some View {
HStack {
Spacer()
CardView()
.hidden()
.matchedGeometryEffect(id: Slot.side, in: namespace)
}
}
}
And here is the card subview:
extension CardTableView {
var card: some View {
CardView()
.rotationEffect(isSide ? .degrees(90): .zero)
.matchedGeometryEffect(
id: isSide ? Slot.side : Slot.top,
in: namespace, isSource: false)
}
}
Notice that now there are no transitions anywhere, and none of the slots knows anything about the other slots. If you want to add another slot, it's a matter of defining another slot subview, adding that new slot subview to the CardTableView ZStack, and updating the card subview to know how to pose itself in the new slot. None of the existing slot subviews are affected. It's O(1) work to add a new slot.

How to adjust the size of shapes in order to fit the screen and align them to center?

In the app I am working on, I have used PocketSVG to read paths stored in .svg file the following way (init):
class ColoringImageViewModel: ObservableObject {
#Published var shapeItemsByKey = [UUID: ShapeItem]()
var shapeItemKeys: [UUID] = []
init(selectedImage: String) {
let svgURL = Bundle.main.url(forResource: selectedImage, withExtension: "svg")!
let _paths = SVGBezierPath.pathsFromSVG(at: svgURL)
for (index, path) in _paths.enumerated() {
let scaledBezier = ScaledBezier(bezierPath: path)
let shapeItem = ShapeItem(path: scaledBezier)
shapeItemsByKey[shapeItem.id] = shapeItem
shapeItemKeys.append(shapeItem.id)
}
}
}
struct ShapeItem: Identifiable {
let id = UUID()
var color: Color = Color.white
var path: ScaledBezier
init(path: ScaledBezier) {
self.path = path
}
}
After reading the paths, I transform them to shapes and place every shape as a ShapeView on ZStack:
struct ShapeView: View {
#Binding var shapeItem: ShapeItem?
var body: some View {
ZStack {
shapeItem!.path
.fill(shapeItem!.color)
shapeItem?.path.stroke(Color.black)
}
}
}
struct ColoringImageView: View {
...
var body: some View {
GeometryReader {
geometry in
ZStack {
ForEach(coloringImageViewModel.shapeItemKeys, id: \.self){ id in
ShapeView(shapeItem: $coloringImageViewModel.shapeItemsByKey[id])
}
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
.background(Color.white)
.contentShape(Rectangle())
.edgesIgnoringSafeArea(.all)
}
}
...
}
On the screen, the paths are placed as shown below:
Instead, I would like to place an arbitrary vector image (split to paths) in the middle of the screen and scale it such that it fits the screen as shown below:
I would appreciate any suggestion to modify the code or implement the additional logic in order to fit the screen and align the image.
Edit
After replacing
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
with
.frame(maxWidth: .infinity, maxHeight: .infinity)
the image is placed as shown below:
It seems like vertically the image is centered although I'm not sure as horizontally it doesn't seem to be centered. Also, I haven't found a solution to fit the image to screen (the example image doesn't fit the screen).
Instead of GeometryReader use the max frame for ZStack as follows
var body: some View {
ZStack {
Color.clear // this gives full screen ZStack
ForEach(coloringImageViewModel.shapeItemKeys, id: \.self){ id in
ShapeView(shapeItem: $coloringImageViewModel.shapeItemsByKey[id])
}
}
.background(Color.white)
.contentShape(Rectangle())
.edgesIgnoringSafeArea(.all)
}

How to know if element comes out the screen in swiftUI?

I try to know if an element comes out the screen in my application. I see that when my element it outside the screen, onDesappear is not trigger. I don't know if there is an other solution to trigger this?
I made an example to explain what I want. I have a circle with an offset to force it out of the screen to a certain degree:
struct ContentView: View {
#ObservedObject var location: LocationProvider = LocationProvider()
#State var heading: Double = 0
var body: some View {
ZStack {
Circle()
.frame(width: 30, height: 30)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(Color.clear)
.offset(y: 300)
.border(Color.black)
.rotationEffect(.degrees(self.heading))
.onReceive(self.location.heading) { heading in
self.heading = heading
}
.onDisappear(perform: { print("Desappear") })
Text("\(self.heading)")
}
}
}
Maybe it's possible with geometryReader?

SwiftUI set position to center of different view

I have two different views, one red rect and one black rect that is always positioned on the bottom of the screen. When I click the red rect it should position itself inside the other rect.
Currently the red rect is positioned statically: .position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50). Is there a way to replace the 210 and 777 dynamically to the position of the black rects center position?
I know that I can use the GeometryReader to get the views size, but how do I use that size to position a different view? Would this even be the right way?
struct ContentView: View {
#State private var tap = false
var body: some View {
ZStack {
VStack {
Spacer()
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
}
.padding()
VStack {
ZStack {
Rectangle()
.foregroundColor(.red)
Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
}
.frame(width: 50, height: 50)
.position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}
}
}
}
First define some structure where to store .center position of some View
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
The build-in mechanism to save such data and expose them to parent View is to set / read (or react) on values which conforms to PreferenceKey protocol.
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
To be able to read the center positions of View we can use well known and widely discussed GeometryReader. I define my PositionReader as a View and here we can simply save its center position in our preferences for further usage. There is no need to translate the center to different coordinate system. To identify the View its tag value must be saved as well
struct PositionReader: View {
let tag: Int
var body: some View {
// we don't need geometry reader at all
//GeometryReader { proxy in
Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.tag, center: anchor)]
}
//}
}
}
To demonstrate how to use all this together see next simple application (copy - paste - run)
import SwiftUI
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
struct PositionReader: View {
let tag: Int
var body: some View {
Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.tag, center: anchor)]
}
}
}
struct ContentView: View {
#State var tag = 0
var body: some View {
ZStack {
VStack {
Color.green.background(PositionReader(tag: 0))
.onTapGesture {
self.tag = 0
}
HStack {
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 1))
.onTapGesture {
self.tag = 1
}
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 2))
.onTapGesture {
self.tag = 2
}
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 3))
.onTapGesture {
self.tag = 3
}
}
}
}.overlayPreferenceValue(Positions.self) { preferences in
GeometryReader { proxy in
Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
}
}
}
func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
let p = preferences.filter({ (p) -> Bool in
p.id == tag
})[0]
return proxy[p.center]
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The code is almost self explanatory, we use .background(PositionReader(tag:)) to save the center position of View (this could be avoided by applying .anchorPreference directly on the View) and
.overlayPreferenceValue(Positions.self) { preferences in
GeometryReader { proxy in
Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
}
}
is used to create small black rectangle which will position itself at center of other Views. Just tap anywhere in green or red rectangles, and the black one will move immediately :-)
Here is view of this sample application running.
Here is possible approach (with a bit simplified your initial snapshot and added some convenient View extension).
Tested with Xcode 11.2 / iOS 13.2
extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
struct ContentView: View {
#State private var tap = false
#State private var bottomRect: CGRect = .zero
var body: some View {
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
.padding()
.rectReader($bottomRect, in: .named("board"))
Rectangle()
.foregroundColor(.red)
.overlay(Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
)
.frame(width: 50, height: 50)
.position(x: self.tap ? bottomRect.midX : 50,
y: self.tap ? bottomRect.midY : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}.coordinateSpace(name: "board")
}
}

State Variables and ForEach Swift

I'm trying to build a simple view in swiftUI with a foreach loop that contains three buttons, and animates these buttons based on a state variable. The problem is, when one button is pressed, all of them animate. I've been working on it for a good few hours but I can't figure out a way to have only one of the buttons in the view animate when it is pressed without just not using a foreach loop and just making three different state variables for three different CardView views. Here is the code for reference:
struct CardView: View {
var flipped:Bool
var body: some View {
VStack {
Image(flipped ? "cardFront" : "cardBack").renderingMode(.original)
.resizable()
.scaledToFit()
.frame(height: 200)
.rotation3DEffect(Angle(degrees: flipped ? 0 : 180), axis: (x: 0, y: 1, z: 0))
.opacity(1)
Text("Flip").padding()
}
}
}
struct ContentView: View {
#State var isFlipped = false
var body: some View {
HStack {
ForEach(0..<3) {number in
Button(action: {
withAnimation(.spring()) {
self.isFlipped.toggle()
}
}) {
CardView(flipped: self.isFlipped)
}
}
}
}
}
Would be very grateful for any pointers as I'm pretty new to all of this. I'd really like to figure out how to make this work in a foreach loop without the aforementioned work around.
Put the #State variable and the Button in the CardView. Here is a basic example:
I needed to use a Rectangle since I didn't have the images you used.
struct CardView: View {
#State var isFlipped = false
var body: some View {
VStack {
Rectangle()
.foregroundColor(self.isFlipped ? Color.blue : Color.red)
.frame(width: 100, height: 200)
.rotation3DEffect(Angle(degrees: self.isFlipped ? 0 : 180), axis: (x: 0, y: 1, z: 0))
.opacity(1)
Button(action: {
withAnimation(.spring()) {
self.isFlipped.toggle()
}
}) {
Text("Flip")
}
}
}
}
struct ContentView: View {
var body: some View {
HStack {
ForEach(0..<3) {number in
CardView()
}
}
}
}
I hope this helps!