I have a simple button, when you press it, It scale for a bit and then back to the first state.Using scaleEffect caused the view redraw and I don't want this because of the performance. Is There any way to prevent swiftUI from redrawing when using scaleEffect?
Note: (to represent the redrawing I use random background for button).
struct ContentView: View {
#State var scale = false
var body: some View {
VStack {
Button {
withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.68, blendDuration: 0.6)) {
scale.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.spring()) {
scale.toggle()
}
}
} label: {
Text("Button")
.padding()
.foregroundColor(.white)
.background(.debug)
.scaleEffect(scale ? 1.1 : 1)
}
}
}
}
#available(iOS 13.0, *)
public extension ShapeStyle where Self == Color {
static var debug: Color {
#if DEBUG
return Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
).opacity(0.7)
#else
return Color(.clear)
#endif
}
}
If your problem is that ContentView (in this case) is being redrawn, then move the button to its own View:
struct ContentView: View {
var body: some View {
VStack {
ScalingButton()
}
}
}
struct ScalingButton: View {
#State var scale = false
var body: some View {
Button {
withAnimation(.interactiveSpring(response: 0.5, dampingFraction: 0.68, blendDuration: 0.6)) {
scale.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.spring()) {
scale.toggle()
}
}
} label: {
Text("Button")
.padding()
.foregroundColor(.white)
.background(.debug)
.scaleEffect(scale ? 1.1 : 1)
}
}
}
Related
I have two texts in a SwiftUI View and two #State wrappers. When the view appears the first text is visible and the second is not. After a few seconds the first text fadeouts and the second text should fade in at the same time. So far so good. Now here is my issue... after the second text fades in, a few seconds later the same second text named Text("HELLO FROM THE OTHER SIDE") has to zoom out. This is the issue i have. How should i change the code so i can trigger the zoom out transition called TextZoomOutTransition as well? Here is the code:
import SwiftUI
struct Transitions: View {
#State changeText: Bool
#State zoomText: Bool
private var TextFadeOut: AnyTransition {
.opacity
.animation(
.easeOut(duration: 0.3)
)
}
private var TextFadeIn: AnyTransition {
.opacity
.animation(
.easeIn(duration: 0.3)
)
}
private var TextZoomOutTransition: AnyTransition {
return .asymmetric(
insertion: .opacity,
removal: .scale(
scale: 1000, anchor: UnitPoint(x: 0.50, y: 0.45))
.animation(
.easeInOut(duration: 2.0)
.delay(0.1)
)
)
}
public var body: some View {
ZStack(alignment: .center) {
Color.clear
VStack(spacing: 24) {
if !changeText {
Text("HELLO THERE")
.transition(TextFadeOut)
} else if !zoomText {
Text("HELLO FROM THE OTHER SIDE")
.transition(TextFadeIn)
}
}
}
.onAppear {
zoomText = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
changeText = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
zoomText = true
}
}
}
}
The zoom out transition already does the fade in during insertion, you just need to add the duration. The same transition fades in at insertion and zooms out at removal.
Here's the code - I corrected some mistakes, if you don't mind (variables start with lower case, #State are variables...):
#State private var changeText = false
#State private var zoomText = false
private var textFadeOut: AnyTransition {
.opacity
.animation(
.easeOut(duration: 0.3)
)
}
private var textZoomOutTransition: AnyTransition {
return .asymmetric(
insertion: .opacity
// Here: add duration for fade in
.animation(
.easeIn(duration: 0.3)
),
removal: .scale(
scale: 1000, anchor: UnitPoint(x: 0.50, y: 0.45))
.animation(
.easeInOut(duration: 2.0)
.delay(0.1)
)
)
}
public var body: some View {
ZStack(alignment: .center) {
Color.clear
VStack(spacing: 24) {
if !changeText {
Text("HELLO THERE")
.transition(textFadeOut)
} else if !zoomText {
Text("HELLO FROM THE OTHER SIDE")
// Use this transition for insertion and removal
.transition(textZoomOutTransition)
}
}
}
.onAppear {
zoomText = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
changeText = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
zoomText = true
}
}
}
I would like to scale some view with animation and go back to the original scale. The way I figured out to do it is by using DispatchQueue.main.asyncAfter with deadline matching the end of scale up animation. As shown in following code. My question is if there is better way to do that (more SwiftUI way, or just simpler).
import SwiftUI
struct ExampleView: View {
#State private var isPulse = false
var body: some View {
VStack {
Text("Hello")
.scaleEffect(isPulse ? 3 : 1)
ZStack {
Capsule()
.frame(width: 100, height: 40)
.foregroundColor(.pink)
Text("Press me")
}
.onTapGesture {
let animationDuration = 0.4
withAnimation(.easeInOut(duration: animationDuration)) {
isPulse.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
withAnimation(.easeInOut) {
isPulse.toggle()
}
}
}
}
}
}
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
ExampleView()
}
}
Pulse animation
You can use two withAnimation statements with the second one that scales the text down using a .delay(animationDuration):
.onTapGesture {
let animationDuration = 0.4
withAnimation(.easeInOut(duration: animationDuration)) {
isPulse.toggle()
}
withAnimation(.easeInOut(duration: animationDuration).delay(animationDuration)) {
isPulse.toggle()
}
}
You could also replace the two calls to withAnimation with a for loop:
.onTapGesture {
let animationDuration = 0.4
for delay in [0, animationDuration] {
withAnimation(.easeInOut(duration: animationDuration).delay(delay)) {
isPulse.toggle()
}
}
}
I'm trying to build a simple animated overlay. Ideally, the dark background fades in (which it's doing now) and the white card slides up from the bottom edge (using .transition(.move(edge: .bottom).
Here's my ContentView.swift file:
struct Overlays: View {
#State var showOverlay = false
var body: some View {
NavigationView {
Button {
withAnimation(.spring()) {
showOverlay.toggle()
}
} label: {
Text("Open overlay")
}
.navigationTitle("Overlay demo")
}
.overlay {
if showOverlay {
CustomOverlay(
overlayPresented: $showOverlay,
overlayContent: "This is a real basic overlay, and it should be sliding in from the bottom."
)
}
}
}
}
And here's my CustomOverlay.swift file:
struct CustomOverlay: View {
#Binding var overlayPresented: Bool
let overlayContent: String
var body: some View {
ZStack(alignment: .bottom) {
overlayBackground
overlayCard
}
}
}
extension CustomOverlay {
var overlayBackground: some View {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
.onTapGesture {
withAnimation(.spring()) {
overlayPresented = false
}
}
}
var overlayCard: some View {
VStack(spacing: 16) {
overlayText
overlayCloseButton
}
.padding()
.frame(maxWidth: .infinity)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.padding()
.transition(.move(edge: .bottom))
}
var overlayText: some View {
Text(overlayContent)
}
var overlayCloseButton: some View {
Button {
withAnimation(.spring()) {
overlayPresented = false
}
} label: {
Text("Close")
}
}
}
This doesn't appear to work. The entire overlay is fading in/out.
https://imgur.com/a/iRzJCsw
If I move the .transition(.move(edge: .bottom) to the CustomOverlay ZStack the entire overlay slides in from the bottom which looks super goofy.
What am I doing wrong?
After some more experimentation, I've found something pretty cool.
Our main ContentView.swift file:
struct Overlays: View {
#State var showOverlay = false
var body: some View {
NavigationView {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
showOverlay.toggle()
}
} label: {
Text("Open overlay")
}
.navigationTitle("Overlay demo")
}
.overlay {
if showOverlay {
// Here's the overlay background, which we can animate independently
OverlayBackground(
overlayPresented: $showOverlay
)
.transition(.opacity)
// Explicit z-index as per https://stackoverflow.com/a/58512696/1912818
.zIndex(0)
// Here's the overlay content card, which we can animate independently too!
OverlayContent(
overlayPresented: $showOverlay,
overlayContent: "This is a real basic overlay, and it should be sliding in from the bottom."
)
.transition(.move(edge: .bottom).combined(with: .opacity))
// Explicit z-index as per https://stackoverflow.com/a/58512696/1912818
.zIndex(1)
}
}
}
}
And here's OverlayBackground.swift (the background):
struct OverlayBackground: View {
#Binding var overlayPresented: Bool
var body: some View {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.25)) {
overlayPresented = false
}
}
}
}
And lastly OverlayContent.swift:
struct OverlayContent: View {
#Binding var overlayPresented: Bool
let overlayContent: String
var body: some View {
VStack {
Spacer()
overlayCard
}
}
}
extension OverlayContent {
var overlayCard: some View {
VStack(spacing: 16) {
overlayText
overlayCloseButton
}
.padding()
.frame(maxWidth: .infinity)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.padding()
}
var overlayText: some View {
Text(overlayContent)
}
var overlayCloseButton: some View {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
overlayPresented = false
}
} label: {
Text("Close")
}
}
}
The result: https://imgur.com/a/1JoMWcs
I would like to implement a toast view in SwiftUI, I am able to do that, but after some time I want to remove the view from the stack. How can we remove current view from the stack?
Here is my code.
// Actual View
struct FormView: View {
var body: some View {
Text("Hello")
.toast() // here is the toast message presentation
}
}
// Toast View
struct ToastMessage<Content: View>: View {
#State var present: Bool = false
let contentView: Content
init( contentView: #escaping () -> Content) {
self.contentView = contentView()
}
var body: some View {
ZStack {
contentView
if present {
HStack {
Text("👋 You're logged in. !!")
.font(.headline)
.foregroundColor(.white)
}
.padding()
.background(Color.black.opacity(0.65))
.cornerRadius(32)
.frame(maxWidth: .infinity, alignment: .center)
.position(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.origin.y + 80)
.transition(.move(edge: .top))
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 2))
}
}
.onAppear {
withAnimation {
present = true
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
present = false
}
}
}
}
}
extension View {
func toast() -> some View {
ToastMessage(contentView: {self})
}
}
Even after 5 seconds, still, this toast view is present in the View's stack. I'm expecting the ToastMessage view should be removed from the FormView after 5 seconds.
Your help would be greatly appreciated and thanks in advance.
Right now, ToastMessage is still in the hierarchy because toast() returns ToastMessage no matter what. Then, within toast message, there's a conditional about whether or not the actual message is displayed.
If you don't want ToastMessage in the hierarchy at all, you'd need to move the #State on level up so that you can use a conditional to determine whether or not it gets displayed.
// Actual View
struct FormView: View {
#State private var showToast = true
var body: some View {
Text("Hello")
.toast(show: $showToast)
.onAppear {
withAnimation { showToast = true }
}
}
}
// Toast View
struct ToastMessage<Content: View>: View {
#Binding var present: Bool
let contentView: Content
init(present: Binding<Bool>, contentView: #escaping () -> Content) {
_present = present
self.contentView = contentView()
}
var body: some View {
ZStack {
contentView
HStack {
Text("👋 You're logged in. !!")
.font(.headline)
.foregroundColor(.white)
}
.padding()
.background(Color.black.opacity(0.65))
.cornerRadius(32)
.frame(maxWidth: .infinity, alignment: .center)
.position(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.origin.y + 80)
.transition(.move(edge: .top))
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 2))
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
withAnimation {
present = false
}
}
}
}
}
extension View {
#ViewBuilder func toast(show: Binding<Bool>) -> some View {
if show.wrappedValue {
ToastMessage(present: show,contentView: {self})
} else {
self
}
}
}
That being said, if you don't want to move the state up, there's not necessarily any harm in keeping ToastMessage in the hierarchy.
I have an image that I apply a 360 rotation on to have the effect of loading/spinning. It works fine until I add Text underneath, the image still spins but it bounces vertically.
Here is code to see it:
import SwiftUI
#main
struct SpinnerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var isAnimating = false
#State var text = ""
var animation: Animation {
Animation.linear(duration: 3.0)
.repeatForever(autoreverses: false)
}
var body: some View {
HStack {
Spacer()
VStack {
Circle()
.foregroundColor(Color.orange)
.frame(height: 100)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.animation(self.isAnimating ? animation : .default)
.onAppear { self.isAnimating = true }
.onDisappear { self.isAnimating = false }
if self.text != "" {
Text(self.text)
}
}
Spacer()
}
.background(Color.gray)
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now()+4, execute: {
self.text = "Test"
})
})
}
}
I replaced the image with a Circle so you won't be able to see the spinning/animation, but you can see the circle bouncing vertically once we set the text. If the text was there from the beginning and it didn't change then all is fine. The issue only happens if the text is added later or if it got updated at some point.
Is there a way to fix this?
Just link animation to dependent state value, like
//... other code
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.animation(self.isAnimating ? animation : .default, value: isAnimating) // << here !!
//... other code
Try using explicit animations instead, with withAnimation. When you use .animation(), SwiftUI sometimes tries to animate the position of your views too.
struct ContentView: View {
#State var isAnimating = false
#State var text = ""
var animation: Animation {
Animation.linear(duration: 3.0)
.repeatForever(autoreverses: false)
}
var body: some View {
HStack {
Spacer()
VStack {
Circle()
.foregroundColor(Color.orange)
.overlay(Image(systemName: "plus")) /// to see the rotation animation
.frame(height: 100)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.onAppear {
withAnimation(animation) { /// here!
self.isAnimating = true
}
}
.onDisappear { self.isAnimating = false }
if self.text != "" {
Text(self.text)
}
}
Spacer()
}
.background(Color.gray)
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now()+4, execute: {
self.text = "Test"
})
})
}
}
Result: