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()
})
)
Related
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
I've been trying to animate a ScrollView using ScrollViewReader and withAnimation.
I can't figure out why these two animations are not working, either from Button or .onAppear?
import SwiftUI
struct ScrollView2: View {
#State private var scrollText = false
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
Button("Scroll to bottom") {
withAnimation(.linear(duration: 30)) {
scrollView.scrollTo(99, anchor: .center)
}
}
ForEach(0..<100) { index in
Text(String(index))
.id(index)
}
.onAppear(perform: {
withAnimation(.linear(duration: 30)) {
scrollView.scrollTo(scrollText ? 99 : 1, anchor: .center)
}
scrollText.toggle()
})
}
}
}
}
It seems like duration doesn't work within withAnimation. Alternatively, I created a function that executes a repeating Timer that fires over 30 seconds, calling scrollTo withAnimation on each loop.
struct ScrollView2: View {
#State private var scrollText = false
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
Button("Scroll to bottom") {
animateWithTimer(proxy: scrollView)
}
ForEach(0..<100) { index in
Text(String(index))
.id(index)
}
}
}
}
func animateWithTimer(proxy: ScrollViewProxy) {
let count: Int = 100
let duration: Double = 30.0
let timeInterval: Double = (duration / Double(count))
var counter = 0
let timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { (timer) in
withAnimation(.linear) {
proxy.scrollTo(counter, anchor: .center)
}
counter += 1
if counter >= count {
timer.invalidate()
}
}
timer.fire()
}
}
Note: There is a delay when you press the button initially because when it tries to scrollTo the first ~40 of numbers, they are already high on the screen and the scrollView doesn't need to scroll anywhere to center them. You can update the timeInterval and counter variables as needed.
Because duration doesn't seem to work with withAnimation yet, I had to be a bit hacky to get the animation effect I wanted.
Here's what I did:
I added a ScrollViewReader to my ScrollView
I used ForEach and added IDs to my items in my ScrollView
Used an .offset and .animation modifiers to animate the
ScrollView itself (not the items in it)
Used .scrollTo within .onAppear to move at launch the ScrollView
to an item further away from the start to allow the user to both
scroll back and forward the items, even with the ScrollView being
itself animated from right to left
Here's what my code looks like:
import SwiftUI
import AVKit
struct ProView: View {
#State private var scrollText = false
var body: some View {
ZStack {
// VStack {
// Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1))
// .ignoresSafeArea(.all)
// }
//
// VStack {
//
// VideoPlayer(player: AVPlayer(url: Bundle.main.url(forResource: "wave-1", withExtension: "mp4")!)) {
// VStack {
// Image("pro-text")
// .resizable()
// .frame(width: 200, height: .infinity)
// .scaledToFit()
// }
// }
// .ignoresSafeArea(.all)
// .frame(width: .infinity, height: 300)
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { value in
HStack(spacing: 5) {
ForEach(0 ..< 100) { i in
HStack {
Image("benefit-1")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-2")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-3")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-4")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-5")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-6")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-7")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-8")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-9")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-10")
.resizable()
.frame(width: 120, height: 120)
}
.id(i)
}
}
.offset(x: scrollText ? -10000 : 20)
.animation(Animation.linear(duration: 300).repeatForever(autoreverses: false))
.onAppear() {
value.scrollTo(50, anchor: .trailing)
scrollText.toggle()
}
}
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ProView()
}
}
I'm developing an app in which I use a tab bar br using HStack{} with Button functions inside of it as shown here: https://i.stack.imgur.com/18Amd.png and I'm trying to round the corners of the bar.
I've tried using .clipShape(RoundedRectangle(cornerRadius:20)) but it will cut out any content outside of the bounds of the background: https://i.stack.imgur.com/zVZb6.png.
Is there a way to round the corners of the HStack background without clipping out the buttons inside of it?
I'm using Xcode 12.0 with SwiftUI.
The code that I'm using is here:
struct TabBar: View {
#Binding var selectedTab : Int
//To assign item color when selected
#State var icon = "unSelect"
var body: some View {
//Icon Design
HStack(alignment: .center) {
Group {
//Home Icon
Button(action: {
self.selectedTab = 0
}) {
HStack(spacing: 5) {
Image("Home").resizable()
.frame(width: 40, height: 40)
}
.padding(5)
}
.foregroundColor(self.selectedTab == 0 ? Color("Select") : Color("unSelect"))
//Chart Icon
Button(action: {
self.selectedTab = 1
}) {
HStack(spacing: 5) {
Image("Chart").resizable()
.frame(width: 40, height: 40)
}
.padding(5)
}
.foregroundColor(self.selectedTab == 1 ? Color("Select") : Color("unSelect"))
//Add Icon
Button(action: {
self.selectedTab = 2
}) {
HStack(spacing: 5) {
Image("Add")
.resizable()
.frame(width: 83.52, height: 83.52, alignment: .top)
.overlay(Circle().stroke(Color("Tab"), lineWidth: 8))
}
.offset(y: -17)
.edgesIgnoringSafeArea(.all)
}
//Log Icon
Button(action: {
self.selectedTab = 3
}) {
HStack(spacing: 54) {
Image("Log").resizable()
.frame(width: 50, height: 50)
}
.padding(4)
}
.foregroundColor(self.selectedTab == 3 ? Color("Select") : Color("unSelect"))
//Settings Icon
Button(action: {
self.selectedTab = 4
}) {
HStack(spacing: 5) {
Image("Settings").resizable()
.frame(width: 40, height: 40)
}
.padding(5)
}
.foregroundColor(self.selectedTab == 4 ? Color("Select") : Color("unSelect"))
}
}
.padding(.vertical, -10)
.padding(.horizontal, 30)
.background(Color("Tab"))
.padding(.top)
.padding(.top)
.clipShape(RoundedRectangle(cornerRadius:20))
}
Try applying the .clipShape to the Color() inside of .background():
.background(Color("Tab").clipShape(RoundedRectangle(cornerRadius:20)))
and remove it from end.
Try adding padding before .clipShape(RoundedRectangle(cornerRadius:20)):
.padding(.top)
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 have a view that I present at the top of other views like a popover view, inside the view I have a couple of buttons. When I add a single button in the overplayed view and tap the button it works. However if I added multiple buttons and try to tap the buttons they don't work. Instead it clicks to the components bellow the view.
I would like to add multiple buttons and click them on the overlay view, I'm not sure what my mistake is on this code:
Here is my code:
struct MenuContent: View {
var body: some View {
List() {
ForEach(0..<2) { _ in
HStack {
ForEach(0..<4) { _ in
Button(action: {
print("tapped button")
}) {
VStack {
Text("Rev")
Image("trash.fill")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
}.background(Color.blue)
}
}
}
}
}
}
OverlayView
struct OverlayMenu: View {
let width: CGFloat
#Binding var show: Bool
var body: some View {
return ZStack {
HStack {
MenuContent()
.frame(width: self.width, height: 160)
.cornerRadius(10, antialiased: false)
.offset(x: self.show ? 0 : -self.width, y: 285)
.animation(.spring())
Spacer()
}
.shadow(radius: 20)
}
}
}
ContentView
struct ContentView: View {
#State var show = true
var body: some View {
OverlayMenu(width: 350,
show: $show)
}
}
I think there is some trouble with List rows and tap gestures on them. You can deal with it if you want or you may try VStack instead of List and use Divider to divide "rows" and taps on the buttons will be handle as you expect. I changed your example a little to show how it works, I think you can handle design by yourself then:
struct MenuContent: View {
#State var hits = 0
var body: some View {
VStack {
Text("\(hits)")
Divider().frame(width: 250)
ForEach(0..<2) { _ in
ButtonsLine(hits: self.$hits)
Divider().frame(width: 250)
}
}
}
}
struct ButtonsLine: View {
#Binding var hits: Int
var body: some View {
HStack {
ForEach(0..<4) { value in
Button(action: {
print("tapped button")
self.hits += value + 1
}) {
ButtonDesign()
}
}
}
}
}
struct ButtonDesign: View {
var body: some View {
VStack {
Text("Rev")
.foregroundColor(.black)
Image(systemName: "trash")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.frame(width: 60, height: 60)
}
.shadow(radius: 20)
}
}
and the result is: