Tap animation on Image SwiftUI - swift

How can I make an animation on an image so when I tap it and hold it scales backwards and when I release it the image return to its original size?
struct ContentView: View {
var body: some View {
Image(systemName: "heart")
.onTapGesture {
// Gesture when held down and released
}
}
}

Here is a demo of possible solution. Tested with Xcode 12 / iOS 14.
struct DemoImageScale: View {
#GestureState private var isDetectingPress = false
var body: some View {
Image("plant")
.resizable().aspectRatio(contentMode: .fit)
.scaleEffect(isDetectingPress ? 0.5 : 1)
.animation(.spring())
.gesture(LongPressGesture(minimumDuration: 0.1).sequenced(before:DragGesture(minimumDistance: 0))
.updating($isDetectingPress) { value, state, _ in
switch value {
case .second(true, nil):
state = true
default:
break
}
})
}
}

Related

SwiftUI - Changing parent #State doesn't update child View

Specific question:
SwiftUI doesn't like us initializing #State using parameters from the parent, but what if the parent holding that #State causes major performance issues?
Example:
How do I make tapping on the top text change the slider to full/empty?
Dragging the slider correctly communicates upwards when the slider changes from full to empty, but tapping the [Overview] full: text doesn't communicate downwards that the slider should change to full/empty.
I could store the underlying Double in the parent view, but it causes major lag and seems unnecessary.
import SwiftUI
// Top level View. It doesn't know anything about specific slider percentages,
// it only knows if the slider got moved to full/empty
struct SliderOverviewView: View {
// Try setting this to true and rerunning.. It DOES work here?!
#State var overview = OverviewModel(state: .empty)
var body: some View {
VStack {
Text("[Overview] full: \(overview.state.rawValue)")
.onTapGesture { // BROKEN: should update child..
switch overview.state {
case .full, .between: overview.state = .empty
case .empty: overview.state = .full
}
}
SliderDetailView(overview: $overview)
}
}
}
// Bottom level View. It knows about specific slider percentages and only
// communicates upwards when percentage goes to 0% or 100%.
struct SliderDetailView: View {
#State var details: DetailModel
init(overview: Binding<OverviewModel>) {
details = DetailModel(overview: overview)
}
var body: some View {
VStack {
Text("[Detail] percentFull: \(details.percentFull)")
Slider(value: $details.percentFull)
.padding(.horizontal, 48)
}
}
}
// Top level model that only knows if slider went to 0% or 100%
struct OverviewModel {
var state: OverviewState
enum OverviewState: String {
case empty
case between
case full
}
}
// Lower level model that knows full slider percentage
struct DetailModel {
#Binding var overview: OverviewModel
var percentFull: Double {
didSet {
if percentFull == 0 {
overview.state = .empty
} else if percentFull == 1 {
overview.state = .full
} else {
overview.state = .between
}
}
}
init(overview: Binding<OverviewModel>) {
_overview = overview
// set inital percent
switch overview.state.wrappedValue {
case .empty:
percentFull = 0.0
case .between:
percentFull = 0.5
case .full:
percentFull = 1.0
}
}
}
struct SliderOverviewView_Previews: PreviewProvider {
static var previews: some View {
SliderOverviewView()
}
}
Why don't I just store percentFull in the OverviewModel?
I'm looking for a pattern so my top level #State struct doesn't need to know EVERY low level detail specific to certain Views.
Running the code example is the clearest way to see my problem.
This question uses a contrived example where an Overview only knows if the slider is full or empty, but the Detail knows what percentFull the slider actually is. The Detail has very detailed control and knowledge of the slider, and only communicates upwards to the Overview when the slider is 0% or 100%
What's my specific case for why I need to do this?
For those curious, my app is running into performance issues because I have several gestures that give the user control over progress. I want my top level ViewModel to store if the gesture is complete or not, but it doesn't need to know the specifics of how far the user has swiped. I'm trying to hide this specific progress Double from my higher level ViewModel to improve app performance.
Here is working, simplified and refactored answer for your issue:
struct ContentView: View {
var body: some View {
SliderOverviewView()
}
}
struct SliderOverviewView: View {
#State private var overview: OverviewModel = OverviewModel(full: false)
var body: some View {
VStack {
Text("[Overview] full: \(overview.full.description)")
.onTapGesture {
overview.full.toggle()
}
SliderDetailView(overview: $overview)
}
}
}
struct SliderDetailView: View {
#Binding var overview: OverviewModel
var body: some View {
VStack {
Text("[Detail] percentFull: \(tellValue(value: overview.full))")
Slider(value: Binding(get: { () -> Double in
return tellValue(value: overview.full)
}, set: { newValue in
if newValue == 1 { overview.full = true }
else if newValue == 0 { overview.full = false }
}))
}
}
func tellValue(value: Bool) -> Double {
if value { return 1 }
else { return 0 }
}
}
struct OverviewModel {
var full: Bool
}
Update:
struct SliderDetailView: View {
#Binding var overview: OverviewModel
#State private var sliderValue: Double = Double()
var body: some View {
VStack {
Text("[Detail] percentFull: \(sliderValue)")
Slider(value: $sliderValue, in: 0.0...1.0)
}
.onAppear(perform: { sliderValue = tellValue(value: overview.full) })
.onChange(of: overview.full, perform: { newValue in
sliderValue = tellValue(value: newValue)
})
.onChange(of: sliderValue, perform: { newValue in
if newValue == 1 { overview.full = true }
else { overview.full = false }
})
}
func tellValue(value: Bool) -> Double {
value ? 1 : 0
}
}
I present here a clean alternative using 2 ObservableObject, a hight level OverviewModel that
only deal with if slider went to 0% or 100%, and a DetailModel that deals only with the slider percentage.
Dragging the slider correctly communicates upwards when the slider changes from full to empty, and
tapping the [Overview] full: text communicates downwards that the slider should change to full/empty.
import Foundation
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var overview = OverviewModel()
var body: some View {
SliderOverviewView().environmentObject(overview)
}
}
// Top level View. It doesn't know anything about specific slider percentages,
// it only cares if the slider got moved to full/empty
struct SliderOverviewView: View {
#EnvironmentObject var overview: OverviewModel
var body: some View {
VStack {
Text("[Overview] full: \(overview.state.rawValue)")
.onTapGesture {
switch overview.state {
case .full, .between: overview.state = .empty
case .empty: overview.state = .full
}
}
SliderDetailView()
}
}
}
// Bottom level View. It knows about specific slider percentages and only
// communicates upwards when percentage goes to 0% or 100%.
struct SliderDetailView: View {
#EnvironmentObject var overview: OverviewModel
#StateObject var details = DetailModel()
var body: some View {
VStack {
Text("[Detail] percentFull: \(details.percentFull)")
Slider(value: $details.percentFull).padding(.horizontal, 48)
.onChange(of: details.percentFull) { newVal in
switch newVal {
case 0: overview.state = .empty
case 1: overview.state = .full
default: break
}
}
}
// listen for the high level OverviewModel changes
.onReceive(overview.$state) { theState in
details.percentFull = theState == .full ? 1.0 : 0.0
}
}
}
enum OverviewState: String {
case empty
case between
case full
}
// Top level model that only knows if slider went to 0% or 100%
class OverviewModel: ObservableObject {
#Published var state: OverviewState = .empty
}
// Lower level model that knows full slider percentage
class DetailModel: ObservableObject {
#Published var percentFull = 0.0
}

How to combine rotation and fade-out animation in SwiftUI

My screen is showing a rotating indicator until the data to be displayed finishes downloading.
I was able to implement the indicator rotation animation correctly.
struct MainView: View {
#ObservedObject var viewModel = MainViewModel()
var body: some View {
VStack(spacing: 0) {
if let data = viewModel.fetchedData {
Text(data.description)
} else {
Spacer()
}
AdBanner()
}
.overlay(Indicator(shown: $viewModel.isLoading))
}
}
struct Indicator: View {
#Binding var shown: Bool
#State private var rotating = false
#ViewBuilder
var body: some View {
if shown {
Image("Ring")
.rotationEffect(.degrees(rotating ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false) {
self.rotating = true
}
}
}
}
}
When the data download is finished, I want this indicator to fade out with its rotation continuing. I have tried many things, but I cannot implement this correctly. For example, it may not animate, or it may fade out but the rotation may be broken.
Not animate:
before:
.overlay(Indicator(shown: $viewModel.isLoading))
after:
.overlay(Indicator(shown: $viewModel.isLoading.animation(.easeOut(duration: 1))))
Rotation broken (The angle is reset to 0 when the fade starts.):
before:
if shown {
Image("Ring")
.rotationEffect(.degrees(rotating ? 360 : 0))
.onAppear {
withAnimation {
self.rotating = true
}
}
}
after:
ZStack {
if shown {
Image("Ring")
.rotationEffect(rotating ? 360 : 0)
.onAppear {
withAnimation {
self.rotating = true
}
}
}
}
.transition(.opacity)
.animation(.easeOut(duration: 1))
Is it possible to fade out the rotation animation without interrupting it?
You need transition, because view (image in this case) is removed from view hierarchy. And transition is animated by container of removing view.
Note: it is better to link every animation to own switching state to avoid affect on other animations
Here is solution. Tested with Xcode 12.1 / iOS 14.1
struct Indicator: View {
#Binding var shown: Bool
#State private var rotating = false
#ViewBuilder
var body: some View {
VStack {
if shown {
Image("Ring")
.rotationEffect(Angle(degrees: rotating ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: rotating)
.transition(.opacity)
.onAppear {
self.rotating = true
}
}
}.animation(.default, value: shown)
}
}

Can you animate a SwiftUI View on disappear?

I have a SwiftUI View which has a custom animation that runs onAppear. I am trying to get the view to animate onDisappear too but it just immediately vanishes.
The below example reproduces the problem - the MyText view should slide in from the left and slide out to the right. The id modifier is used to ensure a new view is rendered each time the value changes, and I have confirmed that both onAppear and onDisappear are indeed called each time, but the animation onDisappear never visibly runs. How can I achieve this?
struct Survey: View {
#State private var id = 0
var body: some View {
VStack {
MyText(text: "\(id)").id(id)
Button("Increment") {
self.id += 1
}
}
}
struct MyText: View {
#State private var offset: CGFloat = -100
let text: String
var body: some View {
return Text(text)
.offset(x: offset)
.onAppear() {
withAnimation(.easeInOut(duration: 2)) {
self.offset = 0
}
}
.onDisappear() {
withAnimation(.easeInOut(duration: 2)) {
self.offset = 100
}
}
}
}
}
Probably you wanted transition, something like
Update: re-tested with Xcode 13.4 / iOS 15.5
struct Survey: View {
#State private var id = 0
var body: some View {
VStack {
MyText(text: "\(id)")
Button("Increment") {
self.id += 1
}
}
}
struct MyText: View {
var text: String
var body: some View {
Text("\(text)").id(text)
.frame(maxWidth: .infinity)
.transition(.slide)
.animation(.easeInOut(duration: 2), value: text)
}
}
}
I'm afraid it can't work since the .onDisappear modifier is called once the view is hidden.
However there is a nice answer here :
Is there a SwiftUI equivalent for viewWillDisappear(_:) or detect when a view is about to be removed?

Get offset of element while moving using animation

How can i get the Y offset of a a moving element at the same time while it's moving?
This is the code that I'm tring the run:
import SwiftUI
struct testView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
self.showPopup.toggle()
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
.animation(.easeInOut)
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
I want to get the Y value of the black squar when the button is clicked the squar will move to a 0 position however I want to detect when the squar did reach the 0 value how can i do that?
Default animation duration (for those animations which do not have explicit duration parameter) is usually 0.25-0.35 (independently of where it is started & platform), so in your case it is completely safe (tested with Xcode 11.4 / iOS 13.4) to use the following approach:
withAnimation(.spring()){
self.offset = .zero
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.animationRunning = false
}
}
SwiftUI doesn't provide a callback with an animation completion, so there are two methods to accomplish detecting when the square has finished animating.
Method 1: Using AnimatableModifier. Here's a well-written Stack Overflow post on how to set that up.
Method 2: Using a Timer to run after the animation has completed. The idea here is to create a scheduled timer that runs after the timer has finished.
struct ContentView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
// add addition here with specified duration
withAnimation(.easeInOut(duration: 1), {
self.showPopup.toggle()
})
// set timer to run after the animation's specified duration
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
// run your completion code here eg.
withAnimation(.easeInOut(duration: 1)) {
self.showPopup.toggle()
}
timer.invalidate()
}
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
}
}
}
}

Repeating Action Continuously In SwiftUI

How can I make an element such as a text field scale up and then down continuously?
I have this:
struct ContentView : View {
#State var size:Double = 0.5
var body: some View {
ZStack {
Text("Hello!")
.padding()
.scaleEffect(size)
}
}
}
I know I need to increase size and then decrease it in some sort of loop but the following cannot be done in SwiftUI:
while true {
self.size += 0.8
sleep(0.2)
self.size -= 0.8
}
A possible solution is to use a (repeating, auto-reversing) animation:
struct ContentView : View {
#State var size: CGFloat = 0.5
var repeatingAnimation: Animation {
Animation
.easeInOut(duration: 2) //.easeIn, .easyOut, .linear, etc...
.repeatForever()
}
var body: some View {
Text("Hello!")
.padding()
.scaleEffect(size)
.onAppear() {
withAnimation(self.repeatingAnimation) { self.size = 1.3 }
}
}
}
Animation.basic is deprecated. Basic animations are now named after their curve types: like linear, etc:
var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}
Source:
https://forums.swift.org/t/swiftui-animation-basic-duration-curve-deprecated/27076
The best way is to create separate animation struct and configure all the options you need(this way your code will be more compact).
To make it more clear and logical use #State property isAnimating. You will be able to stop your animation and resume again and understand when it is in progress.
#State private var isAnimating = false
var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}
var body: some View {
Text("Hello")
.scaleEffect(isAnimating ? 1.5 : 1)
.animation(foreverAnimation)
.onAppear {
self.isAnimating = true
}
}
Using a repeating animation on a view has weird behaviour when used inside if statements.
If you want to do:
if something {
BlinkingView()
}
use a transition with an animation modifier, otherwise the view stays on the screen even after something is set to false.
I made this extension to show a view that repeats change from one state to the next and back:
extension AnyTransition {
static func repeating<T: ViewModifier>(from: T, to: T, duration: Double = 1) -> AnyTransition {
.asymmetric(
insertion: AnyTransition
.modifier(active: from, identity: to)
.animation(Animation.easeInOut(duration: duration).repeatForever())
.combined(with: .opacity),
removal: .opacity
)
}
}
This makes the view appear and disappear with AnyTransition.opacity and while it is shown it switches between the from and to state with a delay of duration.
Example usage:
struct Opacity: ViewModifier {
private let opacity: Double
init(_ opacity: Double) {
self.opacity = opacity
}
func body(content: Content) -> some View {
content.opacity(opacity)
}
}
struct ContentView: View {
#State var showBlinkingView: Bool = false
var body: some View {
VStack {
if showBlinkingView {
Text("I am blinking")
.transition(.repeating(from: Opacity(0.3), to: Opacity(0.7)))
}
Spacer()
Button(action: {
self.showBlinkingView.toggle()
}, label: {
Text("Toggle blinking view")
})
}.padding(.vertical, 50)
}
}
Edit:
When the show condition is true on appear, the transition doesn't start. To fix this I do toggle the condition on appear of the superview (The VStack in my example):
.onAppear {
if self.showBlinkingView {
self.showBlinkingView.toggle()
DispatchQueue.main.async {
self.showBlinkingView.toggle()
}
}
}