How to fix problem of flicking views using .matchedgeometryeffect with animation - swift

Anybody knows how to fix problem of flicking views using .matchedgeometryeffect with animation
Flicking views gif
In my mind I need to disable opacity transition but I dont know how
struct SwiftUIView: View {
#State private var isChanged = false
#Namespace var space
var body: some View {
ZStack {
if !isChanged {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.frame(width: 100, height: 100)
} else {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.frame(width: 300, height: 300)
}
}
.onTapGesture {
withAnimation() {
isChanged.toggle()
}
}
}
}

You can modify the transition:
func transition(_ t: AnyTransition) -> some View
like this :
ZStack {
if !isChanged {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.transition(.scale) // <--- Define the transition to Scale
.frame(width: 100, height: 100)
} else {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.transition(.scale) // <--- Define the transition to Scale
.frame(width: 300, height: 300)
}
}
.onTapGesture {
withAnimation() {
isChanged.toggle()
}
}
If you only need to scale your view you can also apply:
ZStack {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.frame(width: 100, height: 100)
.scaleEffect(isChanged ? 3 : 1)
}
.onTapGesture {
withAnimation() {
isChanged.toggle()
}
}

Related

SwiftUI Animation causes binding conditional view to flash on and off

I have an issue with this setup:
ZStack {
ViewOne
if something ? ViewTwo : nil
}
.animation()
The problem is that when the animation starts, ViewTwo flashes on and off. I'm thinking it has something to do with the view re-rendering or something? But I can't quite figure it out. I've tried moving the animation around, using it on each view separately, combining it all in one view, but it always flashes when it's based on a conditional. I'd like BOTH views to animate together.
Here is a piece of reproducible code snippet.
struct ContentView: View {
#State var isAnimating: Bool
var body: some View {
ZStack {
VStack {
ForEach((1...5).reversed(), id: \.self) {_ in
ZStack {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(.blue)
.frame(width: 200, height: 50)
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
isAnimating
? ButtonImage()
: nil
}
.animation(
.easeInOut(duration: 0.3)
.repeatForever(autoreverses: true)
, value: isAnimating
)
}
Button(action: {
self.isAnimating.toggle()
}, label: {
Text("Animate")
})
}
}
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
}
}
struct ButtonImage: View {
private let buttonSize: CGSize = CGSize(width: 25, height: 25)
var body: some View {
Button(action: {
// to something
}) {
Image(systemName: "flame")
.resizable()
.renderingMode(.template)
.background(Color.red)
.foregroundColor(Color.yellow)
.frame(width: buttonSize.width, height: buttonSize.height)
}
.frame(width: buttonSize.width, height: buttonSize.height)
.offset(x: -buttonSize.width / 2, y: -buttonSize.height / 2)
}
Any ideas of how to resolve this? Showing a view based on a condition, while also animating it without it flashing?
Figured out a way! Not sure if it's the best approach, but it works.
Add the animation to both views separately, then add an opacity modifier to the second view. Here is the code I used.
struct ContentView: View {
#State var isAnimating: Bool
var body: some View {
ZStack {
VStack {
ForEach((1...5).reversed(), id: \.self) {_ in
ZStack {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(.blue)
.frame(width: 200, height: 50)
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
.animation(
.easeInOut(duration: 0.3)
.repeatForever(autoreverses: true)
, value: isAnimating
)
ButtonImage(isAnimating: $isAnimating)
.opacity(isAnimating ? 1 : 0)
}
}
Button(action: {
self.isAnimating.toggle()
}, label: {
Text("Animate")
})
}
}
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
}
}
struct ButtonImage: View {
private let buttonSize: CGSize = CGSize(width: 25, height: 25)
#Binding var isAnimating: Bool
var body: some View {
Button(action: {
// to something
}) {
Image(systemName: "flame")
.resizable()
.renderingMode(.template)
.background(Color.red)
.foregroundColor(Color.yellow)
.frame(width: buttonSize.width, height: buttonSize.height)
}
.frame(width: buttonSize.width, height: buttonSize.height)
.offset(x: -buttonSize.width / 2, y: -buttonSize.height / 2)
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
.animation(
.easeInOut(duration: 0.3)
.repeatForever(autoreverses: true)
, value: isAnimating
)
}
}

Image movement & animation

How can I take a image on SplashView() and make it go to the top right hand corner on Home() view during the animation between the two?
SplashView()
struct SplashView: View {
// 1.
#State var isActive:Bool = false
var body: some View {
VStack {
// 2.
if self.isActive {
// 3.
Home()
.onAppear {
print("IsVerificationSent: \(RequestedVerification)")
}
} else {
// 4.
Image("logo")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.overlay {
RoundedRectangle(cornerRadius: 30)
.stroke(.white, lineWidth: 1)
}
.shadow(color: Color.black.opacity(0.5), radius: 7)
}
}
// 5.
.onAppear {
// 6.
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// 7.
withAnimation {
self.isActive = true
}
}
}
}
}
Home()
struct Home: View {
var body: some View {
VStack {
Image("logo")
.resizable()
.scaledToFill()
.offset(y: 25)
.frame(width: 50, height: 50)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(.white, lineWidth: 1)
.offset(y: 25)
}
.shadow(radius: 2)
Spacer(minLength: 0)
}
}
}
And since I can't put the full Home() view on here, how can I go about adjusting to make it look the best possible?

Matched Geometry Effect in SwiftUI is not working

I'm wondering why my matchedGeometryEffect isn't working as expected. Did I do something wrong? Because this guy (at 3:15) did the same and it looks much smoother. Im running the code on a M1 MacBook Air that means the speed is probably not the problem.
Here is my Code:
struct ContentView: View {
#Namespace var namespace
#State var bool = true
var body: some View {
HStack{
if bool {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(Color.red)
.padding()
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)){
bool.toggle()
}
}
.matchedGeometryEffect(id: "id", in: namespace)
} else {
Rectangle()
.frame(width: 200, height: 200)
.foregroundColor(Color.red)
.matchedGeometryEffect(id: "id", in: namespace)
}
}
}
}
Thanks for your help, Boothosh
In SwiftUI, the modifier order matters. This is the same with matchedGeometryEffect(id:in:properties:anchor:isSource:).
If you want the rectangles to expand in the center, you need the geometries to align in the center. To do this, we will have Rectangles of the same size. This means that both Rectangles will have the matchedGeometryEffect modifier attached, and then styled after.
Code:
HStack {
if bool {
Rectangle()
.matchedGeometryEffect(id: "id", in: namespace) // <- Here
.frame(width: 100, height: 100)
.foregroundColor(Color.red)
.padding()
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
bool.toggle()
}
}
} else {
Rectangle()
.matchedGeometryEffect(id: "id", in: namespace) // <- Here
.frame(width: 200, height: 200)
.foregroundColor(Color.red)
}
}

How can I remove/control fade effect from matchedGeometryEffect?

I have a test code to show the issue, when I am using matchedGeometryEffect, matchedGeometryEffect adds an unwished effect of fade to rendered result, so I like to remove this fading effect or even control it. It could be a good thing when I change the Color of view from one color to other, but in my case it is not good, because the Color is black in all the time.
struct ContentView: View {
#Namespace var animationNamespaceon
#State private var start: Bool = Bool()
var body: some View {
VStack {
Spacer()
circle
Spacer()
Button("update") { start.toggle() }
}
.animation(Animation.linear(duration: 3), value: start)
}
#ViewBuilder var circle: some View {
if start {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.frame(width: 300, height: 300)
}
else {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.frame(width: 50, height: 50)
}
}
}
This is an effect of default .transition(.opacity) which is applied when view is removed from (inserted to) view hierarchy.
I assume you need linear scale transition instead, like
if start {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.transition(.scale(scale: 1))
.frame(width: 300, height: 300)
}
else {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.transition(.scale(scale: 1))
.frame(width: 50, height: 50)
}
Tested with Xcode 13 / iOS 15

Animating a flashing bell in SwiftUI

I am having problems with making a simple systemIcon flash in SwiftUI.
I got the animation working, but it has a silly behaviour if the layout of
a LazyGridView changes or adapts. Below is a video of its erroneous behaviour.
The flashing bell stays in place but when the layout rearranges the bell
starts transitioning in from the bottom of the parent view thats not there anymore.
Has someone got a suggestion how to get around this?
Here is a working example which is similar to my problem
import SwiftUI
struct FlashingBellLazyVGrid: View {
#State var isAnimating = false
#State var showChart = true
var body: some View {
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
HStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{ self.isAnimating = true }
}.padding(50)
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
}
}
}
}
struct FlashingBellLazyVGrid_Previews: PreviewProvider {
static var previews: some View {
FlashingBellLazyVGrid()
}
}
how it looks like before you click the showChart button at the top
After you toggle the button it looks like the bells are erroneously moving into place from the bottom of the screen. and toggling it back to its original state doesn't resolve this bug subsequently.
[
Looks like the animation is basing itself off of the original size of the view. In order to trick it into recognizing the new view size, I used .id(UUID()) on the outside of the grid. In a real world application, you'd probably want to be careful to store this ID somewhere and only refresh it when needed -- not on every re-render like I'm doing:
struct FlashingBellLazyVGrid: View {
#State var showChart = true
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
var body: some View {
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
SeparateComponent()
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
.id(UUID()) //<-- Here
}
}
}
}
struct SeparateComponent : View {
#State var isAnimating : Bool = false
var body: some View {
HStack {
Text("Hello, World!")
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{
self.isAnimating = true
}
}
.padding(50)
}
}
I also separated out the blinking component into its own view, since there were already problematic things happening with the existing logic with onAppear, which wouldn't affect newly-scrolled-to items correctly. This may need refactoring for your particular case as well, but this should get you started.