I am trying to implement a button in swift UI that will show a loading indicator on click. but after the button click, the loading indicator is not animating.
{
Button(action: {
self.isLoading = true
}) {
HStack {
if isLoading {
Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.green, lineWidth: 5)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(.default
.repeatForever(autoreverses: false), value: isLoading)
}
Text( isLoading ? "Processing" : "Submit")
.fontWeight(.semibold)
.font(.title)
}
}
.frame(width: 250, height: 50)
.background(.white)
}
It should be different states: one for transition, one for progress. So it is better to separate progress into different view
Tested with Xcode 13.4 / iOS 15.5
Note: it's better to use linear animation in this case
HStack {
if isLoading {
MyProgress() // << here !!
}
Text( isLoading ? "Processing" : "Submit")
.fontWeight(.semibold)
.font(.title)
}
and
struct MyProgress: View {
#State var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.green, lineWidth: 5)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(.linear
.repeatForever(autoreverses: false), value: isLoading)
.onAppear {
isLoading = true
}
}
}
Test module on GitHub
Related
I have this animation code:
struct CheckmarkAnimation: View {
#State private var isAnimating = false
var body: some View {
ZStack {
Circle()
.trim(to: isAnimating ? 1:0)
.stroke(.green, lineWidth: 3)
.frame(width: 100, height: 100)
.animation(.easeInOut(duration: 1), value: isAnimating)
Image(systemName: "checkmark")
.foregroundColor(.green)
.font(.largeTitle)
.scaleEffect(isAnimating ? 1.5 : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.4).delay(1), value: isAnimating)
}
.onAppear {
isAnimating.toggle()
}
}
}
I would like this view to disappear after the scaling effect on the checkmark ends. How do I do this?
Here are 2 ways to do it:
Change drawing color to .clear after 2 seconds
Here’s a way to have it disappear (turn invisible but still take up space). Change the drawing color to .clear after 2 seconds:
struct CheckmarkAnimation: View {
#State private var isAnimating = false
#State private var color = Color.green
var body: some View {
ZStack {
Circle()
.trim(to: isAnimating ? 1:0)
.stroke(color, lineWidth: 3)
.frame(width: 100, height: 100)
.animation(.easeInOut(duration: 1), value: isAnimating)
Image(systemName: "checkmark")
.foregroundColor(color)
.font(.largeTitle)
.scaleEffect(isAnimating ? 1.5 : 0.001)
.animation(.spring(response: 0.5, dampingFraction: 0.4).delay(1), value: isAnimating)
}
.onAppear {
isAnimating.toggle()
withAnimation(.linear(duration: 0.01).delay(2)) {
color = .clear
}
}
}
}
Use .scaleEffect() on ZStack
Insert this scale animation before the .onAppear:
.scaleEffect(isAnimating ? 0.001 : 1)
.animation(.linear(duration: 0.01).delay(2), value: isAnimating)
.onAppear {
isAnimating.toggle()
}
Note: Use 0.001 instead of 0 in scaleEffect to avoid
ignoring singular matrix: ProjectionTransform
messages in the console.
.scaleEffect() on ZStack is an elegant solution.
struct CheckmarkAnimation: View {
#State private var isAnimating = false
#State private var color = Color.green
var body: some View {
ZStack {
Circle()
.trim(to: isAnimating ? 1:0)
.stroke(color, lineWidth: 3)
.frame(width: 100, height: 100)
.animation(.easeInOut(duration: 1), value: isAnimating)
Image(systemName: "checkmark")
.foregroundColor(color)
.font(.largeTitle)
.scaleEffect(isAnimating ? 1.5 : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.4).delay(1), value: isAnimating)
}
.scaleEffect(isAnimating ? 0 : 1)
.animation(.linear(duration: 0.01).delay(2), value: isAnimating)
.onAppear {
isAnimating.toggle()
}
}
}
This code works and makes the view disappear^^
I am trying to animate the button when user taps on it. It offsets on the side making it look like it's pressed.
If you look at the images you should see why I want to animate it by offsetting it since I have a background behind the button which is offset and the button should match that frame when clicked.
Currently the button animates as shown in the picture when tapped but all of the button animates getting pressed and they don't return to original position after the click happens.
Button before clicked
Button after click
Below is the buttons array:
#State private var isClicked = false
let buttons: [[CalcButton]] = [
[.clear, .plusMinus, .percent, .divide],
[.seven, .eight, .nine, .multiply],
[.four, .five, .six, .minus],
[.one, .two, .three, .add],
[.zero, .doubleZero, .decimal, .equals]
]
ForEach(buttons, id: \.self) { row in
HStack(spacing: 20) {
ForEach(row, id: \.self) { item in
Button(action: {
withAnimation {
self.animation()
}
} , label: {
ZStack {
Rectangle()
.frame(width: buttonWidth(button: item), height: buttonHeight())
.foregroundColor(.backgroundColor)
.offset(x: 7.0, y: 7.0)
Rectangle()
.frame(width: buttonWidth(button: item), height: buttonHeight())
.foregroundColor(.white)
Text(item.rawValue)
.font(.custom("ChicagoFLF", size: 27))
.frame(width: buttonWidth(button: item), height: buttonHeight())
.foregroundColor(.backgroundColor)
.border(Color.backgroundColor, width: 4)
.offset(x: isClicked ? 7 : 0, y: isClicked ? 7 : 0)
}
})
}
}
.padding(.bottom, 10)
}
This is the function to toggle the isClicked state variable
func animation() {
self.isClicked.toggle()
}
You need a selection state for each button. so better to create a custom button.
Here is the demo version code.
Custom Button view
struct CustomButton: View {
var text: String
var action: () -> Void
#State private var isPressed = false
var body: some View {
Button(action: {
// Do something..
}, label: {
ZStack {
Rectangle()
.foregroundColor(.black)
.offset(x: 7.0, y: 7.0)
Rectangle()
.foregroundColor(.white)
Text(text)
.frame(width: 50, height: 50)
.foregroundColor(.black)
.border(Color.black, width: 4)
.offset(x: isPressed ? 7 : 0, y: isPressed ? 7 : 0)
}
})
.buttonStyle(PlainButtonStyle())
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
// Comment this line if you want stay effect after clicked
isPressed = true
})
.onEnded({ _ in
isPressed = false
// // Uncomment below line and comment above line if you want stay effect after clicked
//isPressed.toggle()
action()
})
)
.frame(width: 50, height: 50)
}
}
Usage:
struct DemoView: View {
var body: some View {
HStack(spacing: 10) {
ForEach(0..<10) { index in
CustomButton(text: index.description) {
print("Action")
}
}
}
}
}
If you want to keep your effect after clicked. Just replace this code part.
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
})
.onEnded({ _ in
isPressed.toggle()
action()
})
)
I have a simple loading view on SwiftUI.
When I am displaying this loading screen with .navigationBarHidden(true) on NavigationView.
There is an issue that animation has an unwanted effect on it.
This is my loading animation
struct LoaderThreeDot: View {
var size: CGFloat = 20
#State private var shouldAnimate = false
var body: some View {
HStack(alignment: .center) {
Circle()
.fill(Color.blue)
.scaleEffect(shouldAnimate ? 1.0 : 0.5, anchor: .center)
.animation(Animation.easeInOut(duration: 0.5).repeatForever())
.frame(width: size, height: size)
Circle()
.fill(Color.blue)
.scaleEffect(shouldAnimate ? 1.0 : 0.5, anchor: .center)
.animation(Animation.easeInOut(duration: 0.5).repeatForever().delay(0.3))
.frame(width: size, height: size, alignment: .center)
Circle()
.fill(Color.blue)
.scaleEffect(shouldAnimate ? 1.0 : 0.5, anchor: .center)
.animation(Animation.easeInOut(duration: 0.5).repeatForever().delay(0.6))
.frame(width: size, height: size, alignment: .center)
}
.onAppear {
self.shouldAnimate = true
}
}
}
LoadingView as follow:
struct LoadingView<Content>: View where Content: View {
let title: String
var content: () -> Content
#State var showLoader = false
var body: some View {
ZStack {
self.content()
.disabled(true)
.blur(radius: 3)
Rectangle()
.foregroundColor(Color.black.opacity(0.4))
.ignoresSafeArea()
VStack {
if showLoader {
LoaderThreeDot()
}
Text(title)
.foregroundColor(.black)
.font(.body)
.padding(.top, 10)
}
.padding(.all, 60)
.background(backgroundView)
}
.onAppear {
showLoader.toggle()
}
}
private var backgroundView: some View {
RoundedRectangle(cornerRadius: 12)
.foregroundColor(Color.white)
.shadow(radius: 10)
}
}
And simply presenting it as follow:
NavigationView {
ZStack {
LoadingView(title: "Loading...") {
Rectangle()
.foregroundColor(.red)
}
}
.navigationBarHidden(true)
}
If I remove .navigationBarHidden(true) animation looks ok.
So I am guessing that the animation effect started when the navigation bar was shown and it somehow affecting the animation after the navigation bar is hidden.
Is there any way I can avoid this?
Change your toggle on the main thered.
// Other code
.onAppear() {
DispatchQueue.main.async { //<--- Here
showLoader.toggle()
}
}
// Other code
I have an app were when I tap on a button to open a new view it shows my view because I am using .sheet(), is there a way to make the .sheet() full screen rather than mid way? I tried .present()
.fullScreenCover() and still not working properly. Can anyone help me solve this issue. thanks for the help.
#State var showingDetail = false
Button(action: {
withAnimation {
self.showingDetail.toggle()
}
}) {
Text("Enter")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50)
.background(Color.accentColor)
.cornerRadius(15.0)
.shadow(radius: 10.0, x: 20, y: 10)
}.padding(.top, 50).sheet(isPresented: $showingDetail) {
MainView()
}
You just nee to reorder your modifiers.
Here is the solution provided it will work in iOS 14 +
#State var showingDetail = false
Button(action: {
withAnimation {
self.showingDetail.toggle()
}
}) {
Text("Enter")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50)
.background(Color.accentColor)
.cornerRadius(15.0)
.shadow(radius: 10.0, x: 20, y: 10)
}.fullScreenCover(isPresented: $showingDetail) {
MainView()
.edgesIngoringSafeArea(.all) // if you need to hide navigating and status bar
}
.padding(.top, 50)
Here is the workaround approach for iOS 13.
#State var showingDetail = false
ZStack {
if (!showingDetail) {
Button(action: {
withAnimation {
self.showingDetail.toggle()
}
}) {
Text("Enter")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50)
.background(Color.accentColor)
.cornerRadius(15.0)
.shadow(radius: 10.0, x: 20, y: 10)
}
} else {
// in main view you need to give a button where value of showing detail changes to false
// so clicking on that button will poppet this view
MainView(back: $showingDetail)
.edgesIngoringSafeArea(.all)
.transition(.move(.bottom))
}
}
struct MainView: some View{
#binding back: Bool
var body ....
.....
.....
}
I am trying to transition between two states with SwiftUI.
I have reduced this to a simple example
struct Test2View: View {
#State var isLoading: Bool = true
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color. secondary)
.shadow(radius: 4, x: 2, y: 5)
.frame(width: 300, height: 150, alignment: .center)
if isLoading {
Text("Loading")
.transition(.moveAndFade())
} else {
Text("Content")
.transition(.moveAndFade())
}
}.onTapGesture {
withAnimation {
self.isLoading.toggle()
}
}
}
}
extension AnyTransition {
static func moveAndFade(delay: TimeInterval = 0) -> AnyTransition {
let insertion = AnyTransition.offset(x: 0, y: 15)
.combined(with: .opacity)
let removal = AnyTransition.offset(x: 20, y: 20)
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
This works the way I expected without the RoundedRectangle:
However, as soon as I add the RoundRectangle I lose the removal animation (unless I interrupt the animation, then you can see the animation I was expecting):
Any idea why the RoundedRectangle messes with the animation? I even tried to add .transition(.identity) without any success.
I can't tell the exact reason why the animation changes, but I have found the cause. It's something to do with the size of the frame.
Your version of Test2View's view body:
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.shadow(radius: 4, x: 2, y: 5)
.frame(width: 300, height: 150, alignment: .center)
if isLoading {
Text("Loading")
.transition(.moveAndFade())
} else {
Text("Content")
.transition(.moveAndFade())
}
}
.border(Color.red)
.onTapGesture {
withAnimation {
self.isLoading.toggle()
}
}
Fixed version:
ZStack {
if isLoading {
Text("Loading")
.transition(.moveAndFade())
} else {
Text("Content")
.transition(.moveAndFade())
}
}
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.shadow(radius: 4, x: 2, y: 5)
.frame(width: 300, height: 150, alignment: .center)
)
.border(Color.green)
.onTapGesture {
withAnimation {
self.isLoading.toggle()
}
}
Basically, I used .background instead of creating a VStack.
Comparison images:
The border color is there just to indicate the frame size. You can remove this!