Animation issue when Long-Press is canceled - SwiftUI, iOS 14.5 - animation

I'm still learning and trying to understand the ins and outs of swiftUI. Currently, I focus on understanding gestures and animations and here is an issue that seems unsolvable at my experience level:
The problem:
I experience unexpected behavior if the longPressGesture is canceled, eg. lifting the finger before the minimumDuration of the longPress is exceeded.
The expected result:
In the example above you see me long pressing 2 times and afterward canceling the long-press once. I would expect the red progress bar to scale down again and keep its position centered if the longPress was canceled. But as you can see, the progress bar changes its position.
What is causing this issue and how could it be solved while still using swiftUI and GestureState
Here is the code:
struct DiamondPress: View {
#GestureState var isDetectingLongPress = false
#State var completedLongPress = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 2)
.updating($isDetectingLongPress) { currentState, gestureState, transaction in
gestureState = currentState
transaction.animation = Animation.linear(duration: 2.0)
}
.onEnded { _ in
self.completedLongPress.toggle()
}
}
var body: some View {
ZStack {
Color.offWhite
RoundedRectangle(cornerRadius: 12, style: .continuous)
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(.white)
.scaleEffect(completedLongPress ? 2.0 : 1.0)
.gesture(longPress)
.animation(.easeIn(duration: 0.2))
// here is the red progress bar:
Rectangle()
.frame(
width: !isDetectingLongPress ? CGFloat(5) : CGFloat(100),
height: 5, alignment: .center
)
.foregroundColor(.red)
}
.edgesIgnoringSafeArea(.all)
}
}
Thanks a bunch, any help or hint is highly appreciated.

Related

Keyboard and animation issue in SwiftUI

I have a simple animation
Capsule()
.cornerRadius(25)
.scaleEffect(pulsate ? 1 : 1.2)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true).speed(1.5))
.foregroundColor(Color.red)
.frame(width: 75, height: 27)
.onAppear{
self.pulsate.toggle()
}
Everything works fine and the animation too. But if I open a keyboard, when I close it my view capsule starts jumping instead of just doing the scale effect only.
I really have no idea why and how to solve
Thank you all
You should use value for animation
import SwiftUI
struct ContentView: View {
#State private var pulsate: Bool = Bool()
#State private var stringOfText: String = String()
var body: some View {
Capsule()
.cornerRadius(25)
.scaleEffect(pulsate ? 1 : 1.2)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true).speed(1.5), value: pulsate) // <<: here!
.foregroundColor(Color.red)
.frame(width: 75, height: 27)
.onAppear{
self.pulsate.toggle()
}
TextField("enter text", text: $stringOfText)
}
}

SwiftUI onAppear: Animation duration is ignored

I have found that this trim animation here animate path stroke drawing in SwiftUI works as expected if the MyLines view appears in the ContentView (root view) of the app or when it appears as destination of a navigation link. However, SwiftUI seems to ignore the animation duration if the view appears inside of a custom view - I created an overlay view that transitions in from the right.
I only took that line trimming animation as an example - this bug (if it is one) also seems to occur with other animations, e.g. some view changing its height on appear.
I tried changing the duration. if I double it (e.g. from 2 seconds to 4), the actual animation duration does not seem to change...
struct ContentView: View {
#State var showOverlay: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink("My Lines (Nav Link)", destination: MyLines(height: 200, width: 250))
Button(action: {
self.showOverlay.toggle()
}, label: {
Text("My Lines (overlay)")
})
}
}.overlayView(content: {
VStack {
HStack{
Button(action: { self.showOverlay = false}, label: {
Text("Back")
})
Spacer()
}.padding(.top, 40).padding(.horizontal, 15)
MyLines(height: 200, width: 250)
Spacer()
}
}, background: {
Color(.systemBackground)
}, show: $showOverlay, size: nil, transition: AnyTransition.move(edge: .trailing).animation(.easeInOut(duration: 0.3)))
}
}
Here is the code of MyLine again - I deliberately delay the animation by 1 second so that it becomes clear that the problem is not caused by a too long transition of the overlay view in which the Line view exists.
import SwiftUI
struct MyLines: View {
var height: CGFloat
var width: CGFloat
#State private var percentage: CGFloat = .zero
var body: some View {
Path { path in
path.move(to: CGPoint(x: 0, y: height/2))
path.addLine(to: CGPoint(x: width/2, y: height))
path.addLine(to: CGPoint(x: width, y: 0))
}
.trim(from: 0, to: percentage) // << breaks path by parts, animatable
.stroke(Color.black, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
withAnimation(.easeOut(duration: 2.0)) {
self.percentage = 1.0
}
})
}.padding()
}
}
I have set up a complete project here that should illustrate the problem and also includes the overlay view:
https://github.com/DominikButz/TrimAnimationBug
By the way, this duration problem does not disappear after removing NavigationView and only use the overlay view.
You might wonder, why use the custom overlay instead of the boiler plate navigation? I found that NavigationView causes a lot of problems:
difficult to change the background colour
does not support custom transitions, e.g. with a matched geometry effect (the overlay view I created can appear with any type of transition)
causes weird crashes (e.g. related to matchedGeometryEffect)
prevents the .hideStatusBar modifier to work properly
etc. etc.
Sorry, just found the culprit. I had left an animation(.spring()) modifier on the Overlay View. that seems to confuse SwiftUI somehow. after removing the modifier, it works as expected. Seems it helps writing down the problem to see the solution more easily...

How to hide text behind navigationBar with animation at loading

The code below hide a text view behind a navigationBar based on Network status and it works great. The only problem is that we see the text going to hide behind the navigation bar at the first loading of the page.
How can i fix that? I want the text view to be already hidden at the start (Y Position -20)...
import SwiftUI
struct TestZStackNavigationView: View {
let screenSize: CGRect = UIScreen.main.bounds
#ObservedObject var online = NetStatus()
var body: some View {
NavigationView {
Text("NoNetworkTitle")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: 40, alignment: .center)
.background(Color.red)
.position(x: screenSize.width / 2, y: self.online.connected ? -20 : 20)
.animation(.easeIn(duration: 0.5), value: self.online.connected)
.navigationBarTitle(Text("Navigation Bar Title"), displayMode:.inline)
}
}
}
Ok i got it!
By default 'connected' is set to false in NetStatus() class.
I set it to true and now it's working like i want. ;-)

Can't Animate or Transition SwiftUI Images in Sequence

I have an app with a view where I show images taken with the device's camera (stored
with Core Data) and I want to create the effect of time lapse - say you take photos
of a flower over a period of time and want to show those images in a stream. I have
been able to do that - but I want to have some transition effects between the images
and have not been able to do so. I can apply rotation and a static scale but cannot
fade from 0 to 1 opacity, nor scale up from zero to full frame. I must be missing
something really simple. Here's the code using an array of photos instead of Core Data
for simplicity.
struct ContentView: View {
#State private var activeImageIndex = 0
#State private var startTimer = false
#State private var myAnimationBool = true
let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
let myShots = ["CoolEquipment", "FishAndChipsMedium", "FlatheadLake1", "GlacierBusMedium", "Hike", "HuckALaHuckMedium", "JohnAndRiverMedium", "MacDonaldLodgeLakesideMedium"
]
var body: some View {
GeometryReader { geo in
VStack {
Text("Sequence")
.foregroundColor(.blue)
.font(.system(size: 25))
Image(self.myShots[self.activeImageIndex])
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width - 20, height: geo.size.width - 20, alignment: .center)
.cornerRadius(20)
.shadow(radius: 10, x: 10, y: 10)
//None of these do anything
//.animation(.easeInOut(duration: 0.5))
//.animation(.easeInOut)
//.transition(.opacity)
//.transition(self.myAnimationBool ? .opacity : .slide)
//.transition(.scale)
//.transition(AnyTransition.opacity.combined(with: .slide))
//this works but is static
//.scaleEffect(self.myAnimationBool ? 0.5 : 1.0)
//this works but again is static
//.rotationEffect(.degrees(self.myAnimationBool ? 90 : 0))
.onReceive(self.timer) { t in
print("in fixed timer")
if self.startTimer {
self.activeImageIndex = (self.activeImageIndex + 1) % self.myShots.count
}
}
HStack {
Button(action: {
self.startTimer.toggle()
}) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.frame(width: 200, height: 40)
Text(self.startTimer ? "Stop" : "Start").font(.headline)
}
}
}
.padding()
}//top VStack
}.onDisappear{ self.startTimer = false }
}
}
Any guidance would be appreciated. Xcode 11.3 (11C29)

Getting and setting the position of content in a SwiftUI ScrollView

I have a horizontal scroll view where I'd like to set a position programmatically. Here is the body of the view:
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
...
var body: some View {
GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finalmente.")
.font(Font.title)
.frame(width: scrollWidth, height: scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
}
.frame(width: viewGeometry.size.width, height: scrollHeight)
.padding([.top, .bottom], spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
Spacer(minLength: spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
How do I change this code so that I can get the current position of "The quick brown fox" and restore it at a later time? I'm just trying to do something like we've always done with contentOffset in UIKit.
I can see how a GeometryReader might be useful to get the content's current frame, but there's no equivalent writer. Setting a .position() or .offset() for the scroll view or text hasn't gotten me anywhere either.
Any help would be most appreciated!
I've been playing around with a solution and posted a Gist to what I have working in terms of programmatically setting content offsets https://gist.github.com/jfuellert/67e91df63394d7c9b713419ed8e2beb7
With the regular SwiftUI ScrollView, as far as I can tell, you can get the position with GeometryReader with proxy.frame(in: .global).minY (see your modified example below), but you cannot set the "contentOffset".
Actually if you look at the Debug View Hierarchy you will notice that our content view is embedded in an internal SwiftUI other content view to the scrollview. So you will offset vs this internal view and not the scroll one.
After searching for quite a while, I could not find any way to do it with the SwiftUI ScrollView (I guess will have to wait for Apple on this one). The best I could do (with hacks) is a scrolltobottom.
UPDATE: I previously made a mistake, as it was on the vertical scroll. Now corrected.
class SGScrollViewModel: ObservableObject{
var scrollOffset:CGFloat = 0{
didSet{
print("scrollOffset: \(scrollOffset)")
}
}
}
struct ContentView: View {
public var scrollModel:SGScrollViewModel = SGScrollViewModel()
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
var body: some View {
var topMarker:CGFloat = 0
let scrollTopMarkerView = GeometryReader { proxy -> Color in
topMarker = proxy.frame(in: .global).minX
return Color.clear
}
let scrollOffsetMarkerView = GeometryReader { proxy -> Color in
self.scrollModel.scrollOffset = proxy.frame(in: .global).minX - topMarker
return Color.clear
}
return GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: self.spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
scrollTopMarkerView.frame(height:0)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finally.")
.font(Font.title)
.frame(width: self.scrollWidth, height: self.scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
.background(scrollOffsetMarkerView)
}
.frame(width: viewGeometry.size.width, height: self.scrollHeight)
.padding([.top, .bottom], self.spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
Spacer(minLength: self.spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
}
I messed around with several solutions involving a ScrollView with one or more GeometryReaders, but ultimately I found everything easier if I just ignored ScrollView and rolled my own using View.offset(x:y:) and a DragGesture:
This allows the LinearGradient to be panned by either dragging it like a ScrollView, or by updating the binding, in the case via a Slider
struct SliderBinding: View {
#State var position = CGFloat(0.0)
#State var dragBegin: CGFloat?
var body: some View {
VStack {
Text("\(position)")
Slider(value: $position, in: 0...400)
ZStack {
LinearGradient(gradient: Gradient(colors: [.blue, .red]) , startPoint: .leading, endPoint: .trailing)
.frame(width: 800, height: 200)
.offset(x: position - 400 / 2)
}.frame(width:400)
.gesture(DragGesture()
.onChanged { gesture in
if (dragBegin == nil) {
dragBegin = self.position
} else {
position = (dragBegin ?? 0) + gesture.translation.width
}
}
.onEnded { _ in
dragBegin = nil
}
)
}
.frame(width: 400)
}
}
Clamping the drag operation to the size of the scrolled area is omitted for brevity. This code allows horizontal scrolling, use CGPoints instead of CGSize to implement it horizontally and vertically.

Resources