I have been using the following UIView extension for quite a while to fade text in and out. I have been trying to figure out how to implement this with SwiftUI but so far haven't found anything that exactly addresses this for me. Any help would be greatly appreciated.
extension UIView {
func fadeKey() {
// Move our fade out code from earlier
UIView.animate(withDuration: 3.0, delay: 2.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha
}, completion: nil)
}
func fadeIn1() {
// Move our fade out code from earlier
UIView.animate(withDuration: 1.5, delay: 0.5, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha
}, completion: nil)
}
I assume this is what you wanted. Try this below code:
struct FadeView: View {
#State var isClicked = false
#State var text = "Faded Text"
var body: some View {
VStack {
Text(text) //fade in and fade out
.opacity(isClicked ? 0 : 1)
Button("Click to animate") {
withAnimation {
isClicked.toggle()
}
}
}
}
}
You could use withAnimation function and manipulate the duration, options and delay
struct ContentView: View {
#State var isActive = false
var body: some View {
VStack {
Button("Fade") {
withAnimation(.easeIn(duration: 3.0).delay(2)){
isActive.toggle()
}
}
Rectangle()
.frame(width: 222.0, height: 222.0)
.opacity(isActive ? 0 : 1)
}
}
}
Related
I apply ".animation (.easeIn)" but it is deprecated.
tells me: animation 'has been deprecated in iOS 15.0: use instead with Animation or animation (: value :), but animation (: value :) I don't know which value I need to pass. I can't create animations that start slowly. Can you please help me understand how I can properly use animation (_: value :) to make animation work? I have to create the animation that when I move the slider, the images slowly recompose in space according to how much they are enlarged or reduced.
thank you.
'''
import SwiftUI
struct GalleryView: View {
// MARK: - PROPERTIES
#State private var selectedAnimal: String = "lion"
let animals: [Animal] = Bundle.main.decode("animals.json")
let haptics = UIImpactFeedbackGenerator(style: .medium)
#State private var gridLayout: [GridItem] = [GridItem(.flexible())]
#State private var gridColumn: Double = 3.0
func gridSwitch() {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
// MARK: - BODY
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 30) {
// MARK: - IMAGE
Image(selectedAnimal)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 8))
// MARK: - SLIDER
Slider(value: $gridColumn, in: 2...4, step: 1) // se aumento il valore nello slider ci saranno più sezioni
.padding(.horizontal)
.onChange(of: gridColumn, perform: { value in
gridSwitch()
})
// MARK: - GRID
LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {
ForEach(animals) { item in
Image(item.image)
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.onTapGesture {
selectedAnimal = item.image
haptics.impactOccurred()
}
} //: LOOP
} //: GRID
.animation(.easeIn)
.onAppear(perform: {
gridSwitch()
})
} //: VSTACK
.padding(.horizontal, 10)
.padding(.vertical,50)
} //: SCROLL
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(MotionAnimationView())
}
}
// MARK: - PREVIEW
struct GalleryView_Previews: PreviewProvider {
static var previews: some View {
GalleryView()
}
}
As the error says, .animation() is deprecated, with iOS 15, you need to use .animation(value:) now. where the value is the binding that you want your animation to be triggered, in your case, I assume it is selectedAnimal.
Applying this would be something like
VStack {
}
.animation(.easeIn, value: selectedAnimal)
As discussed in comments, if you want your gridLayout to be animatable, it is a little bit more tricky. Because Arrays has to be Equatable if you want them to be animatable, since extending the GridItem is not a good solution, I came up with this:
delete your .animation method change your gridSwitch function with this:
struct GalleryView: View {
// ... irrelevant code
#State private var isGridChanged = false
func gridSwitch() {
if (isGridChanged) {
withAnimation {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
}
}
else {
gridLayout = Array(repeating: .init(.flexible()), count: Int(gridColumn))
isGridChanged = true
}
}
isGridChanged is required because as you're changing your gridLayout when your View is initialized, it causes a weird bug that everything is getting scaled down when app launches because of withAnimation.
It's easy to have an animation begin when a view appears, by using .onAppear(). But I'd like to perform a repeating animation whenever one of the view's non-State variables changes. An example:
Here is a view that I would like to "throb" whenever its throbbing parameter, set externally, is true:
struct MyCircle: View {
var throbbing: Bool
#State var scale = 1.0
var body: some View {
Circle()
.frame(width: 100 * scale, height: 100 * scale)
.foregroundColor(.blue)
.animation(.easeInOut.repeatForever(), value: scale)
.onAppear { scale = 1.2 }
}
}
Currently the code begins throbbing immediately, regardless of the throbbing variable.
But imagine this scenario:
struct ContentView: View {
#State var throb: Bool = false
var body: some View {
VStack {
Button("Throb: \(throb ? "ON" : "OFF")") { throb.toggle() }
MyCircle(throbbing: throb)
}
}
}
It looks like this:
Any ideas how I can modify MyCircle so that the throbbing starts when the button is tapped and ends when it is tapped again?
You can use onChange to watch throbbing and then assign an animation. If true, add a repeating animation, and if false, just animate back to the original scale size:
struct MyCircle: View {
var throbbing: Bool
#State private var scale : CGFloat = 0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.scaleEffect(1 + scale)
.foregroundColor(.blue)
.onChange(of: throbbing) { newValue in
if newValue {
withAnimation(.easeInOut.repeatForever()) {
scale = 0.2
}
} else {
withAnimation {
scale = 0
}
}
}
}
}
There're two interesting things that i've just found when trying to achieve yours target.
Animation will be added when the value changed
Animation that added to the view cannot be removed, and we need to drop the animated view from the view hiearachy by remove its id, new view will be created with zero animations.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var throbbling = false
#State var circleId = UUID()
var body: some View {
VStack {
Toggle("Throbbling", isOn: $throbbling)
circle.id(circleId)
.scaleEffect(throbbling ? 1.2 : 1.0)
.animation(.easeInOut.repeatForever(), value: throbbling)
.onChange(of: throbbling) { newValue in
if newValue == false {
circleId = UUID()
}
}
}.padding()
}
#ViewBuilder
var circle: some View {
Circle().fill(.green).frame(width: 100, height: 100)
}
}
PlaygroundPage.current.setLiveView(ContentView())
this simple code of x offset animation causes a text to move also on y axis when the text is part of a button (on tvOS 14.7)
struct Animate: View
{
#State var scrollText: Bool = false
var body: some View {
Button(action: {}, label: {
Text("This is My Text !")
.offset(x: scrollText ? 0 : 200, y: 0)
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: true))
.onAppear {
self.scrollText.toggle()
}
})
}
}
see the animation behavior here:
marquee gone wrong
How can i stop the y axis movement ?
You always need to set a value for your animations. Otherwise, SwiftUI will animate all changes, include unwanted positioning.
struct Animate: View {
#State var scrollText: Bool = false
var body: some View {
Button(action: {}) {
Text("This is My Text !")
.offset(x: scrollText ? 0 : 200, y: 0)
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: true), value: scrollText) /// only update animation when `scrollText` changes
.onAppear {
// DispatchQueue.main.async { /// you might also need this
self.scrollText.toggle()
// }
}
}
}
}
Result:
This video might also be helpful
Presenting a modal sheet in SwiftUI MacOS has a nice slide in animation, but when dismissed it just disappear.
I would like to add a slide out animation to the dismissal of the sheet.
Adding the slideout on the content of the sheet obviously doesn’t work, as the content slides out, but the sheet frame remains until dismissed.
I might be missing something obvious here, but how can I add animation to the sheet itself?
struct ModalSheet: View {
#Binding var visible: Bool
#State var isShowing: Bool = true
var body: some View {
let s = "Click me to dismiss " + String(isShowing);
VStack {
Text(s)
}
.frame(width: 200, height: 100, alignment: .center)
.background(Color.green.opacity(0.5))
.onTapGesture {
isShowing.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
visible.toggle()
}
}
.offset(y: self.isShowing ? 0 : CGFloat(-110)) // <-- this should be on the sheet itself
.animation(self.isShowing ? .none : .default)
}
}
struct ModalTestAnimation: View {
#State var visible: Bool = false;
var body: some View {
VStack {
Text("Click me")
.onTapGesture {
visible = true
}
}
.frame(width: 250, height: 150)
.sheet(isPresented: $visible, content: {
ModalSheet(visible: $visible)
})
}
}
I did take a look at this, but it doesn’t work for me
SwiftUI sheet not animating dismissal on macOS Big Sur
I have a view sitting on top of a mapView (in a ZStack) and want to be able to have the green, upper view fade in and out with the .easeInOut animation modifier applied to the view's opacity. As you can see in the gif, it fades in nicely but disappears abruptly.
If I remove the mapView then all is good. If I replace the mapView with a simple Rectangle() then the problem returns so I believe it has something to do with the ZStack rather than the map. Funnily enough I'm actually using Mapbox rather than MapKit (as in the code below for simplicity) and the fade/abrupt disappear behaviour is reversed.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var show = false
var body: some View {
VStack {
ZStack {
MapView()
if show {
LabelView()
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
}
}
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
}
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.mapType = .standard
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct LabelView: View {
var body: some View {
Text("Hi there!")
.padding(10)
.font(.title)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.green).shadow(color: .gray, radius: 3))
}
}
I have tried using alternative animation code, replacing the LabelView transition with:
.transition(.opacity)
and changing the button code to:
Button("Animate") {
withAnimation(.easeInOut(duration: 1.0)) {
self.show.toggle()
}
}
but the same behaviour appears each time. I'm guessing this is a SwiftUI bug but couldn't find any previous reference.
Here is working solution. Tested with Xcode 11.4 / iOS 13.4.
As it seen in demo presence of transparent label does not affect functionality of map view.
var body: some View {
VStack {
ZStack {
MapView()
LabelView().opacity(show ? 1 : 0) // here !!
}.animation(.easeInOut(duration: 1.0))
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
Another alternate, actually with the same visual effect is to embed LabelView into container and apply transition to it (it must be left something to render view disappearing)
var body: some View {
VStack {
ZStack {
MapView()
VStack { // << here !!
if show {
LabelView()
}
}.transition(.opacity).animation(.easeInOut(duration: 1.0))
}
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
try this: -> just added zIndex ...everything else the same
struct ContentView: View {
#State private var show = false
var body: some View {
VStack {
ZStack {
MapView().zIndex(0)
if show {
LabelView()
.zIndex(1)
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
}
}
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
}
and read this:
Transition animation not working in SwiftUI