Using SwiftUI, I'm trying to show an animated image and hide the text when the user clicks the button. Here is my code:
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
}
else {
Text("Continue")
}
}
}
}
The Continue text appears and when clicked it disappears (as expected) and the image shows, but no animation. Any ideas what I might be doing wrong.
Thanks
or try this:
struct ImageView : View {
#State private var showAction = false
var body: some View {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showAction ? 360.0 : 0.0))
.animation(self.showAction ? Animation.linear(duration: 1.5).repeatForever(autoreverses: false) : nil)
.onAppear() {
self.showAction = true
}
}
}
struct ContentView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
ImageView()
}
else {
Text("Continue")
}
}
}
}
}
You can use .opacity instead of if...else statements, in this case rotation works on image. But in canvas I still see some glitches, if tap button several times but I didn't try on real device (behavior there can be other). Hope it'll help:
struct AnimatableView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
ZStack {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
.opacity(self.showingActivity ? 1 : 0)
Text("Continue")
.opacity(self.showingActivity ? 0 : 1)
}
}
}
}
Related
Progress View doesn't show on second + load when trying to do pagination. When I scroll to the bottom the progress view will appear once. But all the other times it doesn't. This only seems to occur when im using some sort of animation.
If I just have a static text like "Loading..." it works as expected. I added the section group where it checks a condition to verify if it should be presented or not. Not sure if I'm supposed to use something like "stop animating" like the loading indicator has in UIKit
struct ContentView: View {
#State var movies: [Movie] = []
#State var currentPage = 1
#State private var isLoading = false
var body: some View {
NavigationView {
List {
Section {
} header: {
Text("Top Movies")
}
ForEach(movies) { movie in
HStack(spacing: 8) {
AsyncImage(url: movie.posterURL, scale: 5) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
.cornerRadius(10)
} placeholder: {
ProgressView()
.frame(width: 100)
}
VStack(alignment: .leading, spacing: 10) {
Text(movie.title)
.font(.headline)
Text(movie.overview)
.lineLimit(5)
.font(.subheadline)
Spacer()
}
.padding(.top, 10)
}
.onAppear {
Task {
//Implementing infinite scroll
if movie == movies.last {
isLoading = true
currentPage += 1
movies += await loadMovies(page: currentPage)
isLoading = false
}
}
}
}
Section {
} footer: {
if isLoading {
HStack {
Spacer()
ProgressView()
.tint(.green)
Spacer()
}
} else {
EmptyView()
}
}
}
.navigationTitle("Movies")
.navigationBarTitleDisplayMode(.inline)
.listStyle(.grouped)
.task {
movies = await loadMovies()
}
.refreshable {
movies = await loadMovies()
}
}
}
}
even when I look at the view hierarchy its like the progress view square is there but the icon / loading indicator isn't showing:
If I add the overlay modifier it works but I don't like doing this because when I scroll back up before the content finishes loading the spinner is above the list view:?
.overlay(alignment: .bottom, content: {
if isLoading {
HStack {
Spacer()
ProgressView()
.tint(.green)
Spacer()
}
} else {
EmptyView()
}
})
We also had this problem, we think it is a bug of ProgressView. Our temporary correction is to identify the ProgressView with a unique id in order to render it again.
struct ContentView: View {
#State var id = 0
var body: some View {
ProgressView()
.id(id)
.onAppear {
...
id += 1
}
}
}
I've been trying to add a shrinking affect on buttons while they're being pressed, and I was able to establish it with with LongPressGesture
But at the same time, I noticed that this also stops the container from being scrolled when the box is pressed to start scrolling, for example, I can scroll when I press an empty space first to start scrolling, but I can't scroll when I press the box first to start scrolling.
I'm using Xcode 14.0.1 on macOS Monterey 12.6 and I tested the code both in virtual and physical phones running on iOS 16.
And here's a simplified version of my code,
struct TestView: View {
#GestureState var isDetectingLongPress = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 25) {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(Color.blue)
.frame(height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
}
)
}
}
}
}
How can I fix this? Thanks!
Approach 1:
Add a modifier that delays the recognition of the tap gesture. If a user starts their scroll on the button, the user will still be able to scroll. The downside to this is if the user wants to tap the button, it will be slightly delayed.
This approach was found here
One thing you'll notice is that I moved the RoundedRectange you created into its own view. Hope this makes things easier to read!
import SwiftUI
struct ScrollTest: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 25) {
AnimatedButtonView()
}
}
}
}
struct AnimatedButtonView: View {
#GestureState var isDetectingLongPress = false
var body: some View {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(Color.blue)
.frame(height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
.delaysTouches(for: 0.1) {
//some code here, if needed
}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("pooop")
})
}
}
extension View {
func delaysTouches(for duration: TimeInterval = 0.25, onTap action: #escaping () -> Void = {}) -> some View {
modifier(DelaysTouches(duration: duration, action: action))
}
}
fileprivate struct DelaysTouches: ViewModifier {
#State private var disabled = false
#State private var touchDownDate: Date? = nil
var duration: TimeInterval
var action: () -> Void
func body(content: Content) -> some View {
Button(action: action) {
content
}
.buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
.disabled(disabled)
}
}
fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
#Binding var disabled: Bool
var duration: TimeInterval
#Binding var touchDownDate: Date?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: handleIsPressed)
}
private func handleIsPressed(isPressed: Bool) {
if isPressed {
let date = Date()
touchDownDate = date
DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
if date == touchDownDate {
disabled = true
DispatchQueue.main.async {
disabled = false
}
}
}
} else {
touchDownDate = nil
disabled = false
}
}
}
Approach 2:
Similarly hacky to approach 1. Add a .onTapGesture before .gesture. Downside is that it adds a delay and I am not sure if that is behavior you are looking for:
import SwiftUI
struct ScrollTest: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 25) {
AnimatedButtonView()
}
}
}
}
struct AnimatedButtonView: View {
#GestureState var isDetectingLongPress = false
var body: some View {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(Color.blue)
.frame(height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
.onTapGesture {}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("pooop")
})
}
}
RectangleView has a slide animation, his child TextView has a rotation animation. I suppose that RectangleView with his child(TextView) as a whole slide(easeInOut) into screen when Go! pressed, and TextView rotate(linear) forever. But in fact, the child TextView separates from his parent, rotating(linear) and sliding(linear), and repeats forever.
why animation applied to parent effect child animation?
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
if go {
RectangleView()
.transition(.slide)
.animation(.easeInOut)
}
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
If there are several simultaneous animations the generic solution (in majority of cases) is to use explicit state value for each.
So here is a corrected code (tested with Xcode 12.1 / iOS 14.1, use Simulator or Device, Preview renders some transitions incorrectly)
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
VStack { // container needed for correct transition !!
if go {
RectangleView()
.transition(.slide)
}
}.animation(.easeInOut, value: go) // << here !!
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating) // << here !!
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
PREVIOUS Q: swiftui, animation applied to parent effect child animation
Now the TextView has its own state. RectangleView with TextView slide into screen in 3 seconds, but state of TextView changed after a second while sliding. Now you can see TextView stops sliding, go to the destination at once. I Just want RectangleView with TextView as a whole when sliding, and TextView rotates or keeps still as I want.
import SwiftUI
struct RotationEnvironmentKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var rotation: Bool {
get { return self[RotationEnvironmentKey] }
set { self[RotationEnvironmentKey] = newValue }
}
}
struct AnimationTestView: View {
#State private var go = false
#State private var rotation = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
rotation.toggle()
print("set rotation = \(rotation)")
}
}
Group {
if go {
RectangleView()
.transition(.slide)
.environment(\.rotation, rotation)
}
}.animation(.easeInOut(duration: 3.0), value: go)
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#Environment(\.rotation) var rotation
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
print("refresh, rotation = \(rotation)"); return
HStack {
Spacer()
if rotation {
Text("R")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
} else {
Text("S")
}
}
}
}
I found the issue here: cannot use if cause in TextView, because when rotation changed, will create a new Text in new position, so we can see the R 'jump' to the right position.
code as following:
Text(rotation ? "R" : "S")
.foregroundColor(rotation ? .blue : .white)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(rotation ? animation : nil, value: animationRotating)
.onChange(of: rotation) { newValue in
animationRotating = true
}
Updated to Xcode beta-3, Popover was deprecated... having one hell of a time trying to figure out how to make it work again!?!?
It no longer "pops up" it slides up from the bottom.
It's no longer positioned or sized correctly, takes up the whole screen.
Once dismissed, it never wants to appear again.
This was the old code, that worked perfectly...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
#State private var showPositions = false
var body: some View {
HStack {
Spacer()
Button(action: { self.showPositions = true } ) {
Text("Position")
}
.presentation(showPositions ? Popover(content: MultiPicker(items: Exercise.Position.allCases, selected:$filter.positions),
dismissHandler: { self.showPositions = false })
: nil)
}
.padding()
}
}
And this is the new code...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
#State private var showPositions = false
var body: some View {
HStack {
Spacer()
Button(action: { self.showPositions = true } ) {
Text("Position")
}
.popover(isPresented: $showPositions) {
MultiPicker(items: Exercise.Position.allCases, selected:self.$filter.positions)
.onDisappear { self.showPositions = false }
}
}
.padding()
}
}
I ended up using PresentationLink just so I can move forward with everything else...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
var body: some View {
HStack {
Spacer()
PresentationLink(destination: MultiPicker(items: Exercise.Position.allCases, selected:$filter.positions)) {
Text("Position")
}
}
.padding()
}
}
It works, as far as testing is concerned, but it's not a popover.
Thanks for any suggestions!
BTW, this code is being in the iPad simulator.
On OSX the code below works fine
struct ContentView : View {
#State var poSelAbove = false
#State var poSelBelow = false
#State var pick : Int = 1
var body: some View {
let picker = Picker(selection: $pick, label: Text("Pick option"), content:
{
Text("Option 0").tag(0)
Text("Option 1").tag(1)
Text("Option 2").tag(2)
})
let popoverWithButtons =
VStack {
Button("Not Dismiss") {
}
Divider()
Button("Dismiss") {
self.poSelAbove = false
}
}
.padding()
return VStack {
Group {
Button("Show button popover above") {
self.poSelAbove = true
}.popover(isPresented: $poSelAbove, arrowEdge: .bottom) {
popoverWithButtons
}
Divider()
Button("Show picker popover below") {
self.poSelBelow = true
}.popover(isPresented: $poSelBelow, arrowEdge: .top) {
Group {
picker
}
}
}
Divider()
picker
.frame(width: 300, alignment: .center)
Text("Picked option: \(self.pick)")
.font(.subheadline)
}
// comment the line below for iOS
.frame(width: 800, height: 600)
}
On iOS (iPad) the popover will appear in a strange transparent full screen mode. I don't think this is intended. I have added the problem to my existing bug report.