Disable default button click animation in SwiftUi - animation

How can i disable the default Button click animation in SwiftUI and Swift 5? I tried to add .animation(.nil) to the button, without any changes.
I know that you can do the following:
Button(action: {}) { Capsule() }
.buttonStyle(NoAnim())
struct NoAnim: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
}
Does anybody know a smarter way?

If I correctly understood your question, then it is better to use just
Capsule()
.onTapGesture {
// << action here !!
}

iOS 13.x, Swift 5.
So you want something that is clickable, but not a button. Just use a label with a onTapGesture on it and then you can add whatever animation you like.
Alternatively you could use the onDrag gesture like this too. This will update the dragLocation as soon as you touch it. So it is like touch event. It also doesn't have any animation liked to it either. That you can add if you so wish.
Text("Hello World")
.accessibility(label: Text("Button"))
.gesture(
DragGesture(minimumDistance: 5, coordinateSpace: .global)
.onChanged { value in
self.dragLocation = value.location
}
.onEnded { _ in
self.dragLocation = .zero
}
)

Related

SwiftUI Tooltip on hover

SwiftUI provides the .help() modifier but it is too small, cannot be customised and takes too long to appear to actually serve its intended purpose. I would like to create a tooltip that appears immediately on hover and is larger, similar to the one that appears on hovering on an icon in the Dock.
Something like this:
Is this possible to create from SwiftUI itself? I've tried using a popover but it prevents hover events from propagating once its open, so I can't make it close when the mouse moves away.
Solution #1: Check for .onHover(...)
Use the .onHover(perform:) view modifier to toggle a #State property to keep track of whether your tooltip should be displayed:
#State var itemHovered: Bool = false
var body: some View {
content
.onHover { hover in
itemHovered = hover
}
.overlay(
Group {
if itemHovered {
Text("This is a tooltip")
.background(Color.white)
.foregroundColor(.black)
.offset(y: -50.0)
}
}
)
}
Solution #2: Make a Tooltip Wrapper View
Create a view wrapper that creates a tooltip view automatically:
struct TooltipWrapper<Content>: View where Content: View {
#ViewBuilder var content: Content
var hover: Binding<Bool>
var text: String
var body: some View {
content
.onHover { hover.wrappedValue = $0 }
.overlay(
Group {
if hover.wrappedValue {
Text("This is a tooltip")
.background(Color.white)
.foregroundColor(.black)
.offset(y: -50.0)
}
}
)
}
}
Then you can call with
#State var hover: Bool = false
var body: some View {
TooltipWrapper(hover: $hover, text: "This is a tooltip") {
Image(systemName: "arrow.right")
Text("Hover over me!")
}
}
From this point, you can customize the hover tooltip wrapper to your liking.
Solution #3: Use my Swift Package
I wrote a 📦 Swift Package that makes SwiftUI a little easier for personal use, and it includes a tooltip view modifier that boils the solution down to:
import ShinySwiftUI
#State var showTooltip: Bool = false
var body: some View {
MyView()
.withTooltip(present: $showTooltip) {
Text("This is a tooltip!")
}
}
Notice you can provide your own custom views in the tooltip modifier above, like Image or VStack. Alternatively, you could use HoverView to get a stateful hover variable to use solely within your view:
HoverView { hover in
Rectangle()
.foregroundColor(hover ? .red : .blue)
.overlay(
Group {
if hover { ... }
}
)
}

SwiftUI 2.0 TabView disable swipe to change page

I have a TabView thats using the swiftUI 2.0 PageTabViewStyle. Is there any way to disable the swipe to change pages?
I have a search bar in my first tab view, but if a user is typing, I don't want to give the ability to change they are on, I basically want it to be locked on to that screen until said function is done.
Here's a gif showing the difference, I'm looking to disable tab changing when it's full screen in the gif.
https://imgur.com/GrqcGCI
Try something like the following (tested with some stub code). The idea is to block tab view drag gesture when some condition (in you case start editing) happens
#State var isSearching = false
// ... other code
TabView {
// ... your code here
Your_View()
.gesture(isSearching ? DragGesture() : nil) // blocks TabView gesture !!
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
I tried Asperis's solution, but I still couldn't disable the swiping, and adding disabled to true didn't work since I want the child views to be interactive. The solution that worked for me was using Majid's (https://swiftwithmajid.com/2019/12/25/building-pager-view-in-swiftui/) custom Pager View and adding a conditional like Asperi's solution.
Majid's PagerView with conditional:
import SwiftUI
struct PagerView<Content: View>: View {
let pageCount: Int
#Binding var canDrag: Bool
#Binding var currentIndex: Int
let content: Content
init(pageCount: Int, canDrag: Binding<Bool>, currentIndex: Binding<Int>, #ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._canDrag = canDrag
self._currentIndex = currentIndex
self.content = content()
}
#GestureState private var translation: CGFloat = 0
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
self.content.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentIndex) * geometry.size.width)
.offset(x: self.translation)
.animation(.interactiveSpring(), value: currentIndex)
.animation(.interactiveSpring(), value: translation)
.gesture(!canDrag ? nil : // <- here
DragGesture()
.updating(self.$translation) { value, state, _ in
state = value.translation.width
}
.onEnded { value in
let offset = value.translation.width / geometry.size.width
let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
}
)
}
}
}
ContentView:
import SwiftUI
struct ContentView: View {
#State private var currentPage = 0
#State var canDrag: Bool = true
var body: some View {
PagerView(pageCount: 3, canDrag: $canDrag, currentIndex: $currentPage) {
VStack {
Color.blue
Button {
canDrag.toggle()
} label: {
Text("Toogle drag")
}
}
VStack {
Color.red
Button {
canDrag.toggle()
} label: {
Text("Toogle drag")
}
}
VStack {
Color.green
Button {
canDrag.toggle()
} label: {
Text("Toogle drag")
}
}
}
}
}
Ok I think it is possible to block at least 99% swipe gesture if not 100% by using this steps:
and 2.
Add .gesture(DragGesture()) to each page
Add .tabViewStyle(.page(indexDisplayMode: .never))
SwiftUI.TabView(selection: $viewModel.selection) {
ForEach(pages.indices, id: \.self) { index in
pages[index]
.tag(index)
.gesture(DragGesture())
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Add .highPriorityGesture(DragGesture()) to all remaining views images, buttons that still enable to drag and swipe pages
You can also in 1. use highPriorityGesture but it completely blocks drags on each pages, but I need them in some pages to rotate something
For anyone trying to figure this out, I managed to do this by setting the TabView state to disabled.
TabView(selection: $currentIndex.animation()) {
Items()
}.disabled(true)
Edit: as mentioned in the comments this will disable everything within the TabView as well

SwiftUI: Action of custom button not triggered

Testing some stuff on tvOS with SwiftUI. When i add a custom style to a button, the "action" of the button is not getting triggered. In this example, i wanna print "pressed", but adding functions is not working either.
Is it possible to implement an custom action trigger aswell or what am i doing wrong?
For clarification why i wanna have a custom button style.
When i dont use any buttonstyle, the "action" is working, but the "onFocusChange" function is never getting called. WHICH I NEED!
But.. when i use a buttonstyle, the onFocusChange is working but the action is not.....
struct CustomButton: View {
#State private var buttonFocus: Bool = false
var body: some View {
VStack(alignment: .center){
Button(action: {
print("pressed")
})
{
Text("Save")
}
.buttonStyle(TestButtonStyle(focused: buttonFocus))
.focusable(true, onFocusChange: { (changed) in
self.buttonFocus.toggle()
})
}
}
}
Buttonstyle:
struct TestButtonStyle: ButtonStyle {
let focused: Bool
public func makeBody(configuration: TestButtonStyle.Configuration) -> some View {
configuration.label
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 5).fill(Color.red))
.scaleEffect(focused ? 1.5 : 1.0)
}
}
Its working with SwiftUI 2.0 now.
You can use the following environment Object on your View you want to be focused.
Also, add a State or Binding to bubble up to your parent view.
#Environment(\.isFocused) var isFocused: Bool
#Binding var focusedValue: Bool
Then, you can call the following modifier, which gets called when the View is getting focused or not. Here you change your Binding or State variable.
.onChange(of: isFocused, perform: { value in
self.focusedValue = value
})
Finally, you can use your Binding or State to modify your View.
.scaleEffect(self.focused ? 1.1 : 1)
.animation(.linear(duration: 0.1))

Can't do a Simple Navigate to View and back SwiftUI Navigation Bar Button

I'm trying to do a simple SwiftUI navigation from one view to another and back using
a bar button item. I have tried three different approaches to calling a new view.
Using a Button in the body view works, but using NavigationBarItems in the navigation
bar fails in two different ways.
Here's the start view:
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
VStack {
Text("This is the content view")
.navigationBarTitle("Nav Title")
//this works ONCE only:
.navigationBarItems(trailing: Button(action: {self.showSecondView.toggle()}) {
Text("SecondView")
})
//this always fails on return to contentview with error:
//Tried to pop to a view controller that doesn't exist
// .navigationBarItems(trailing:
// NavigationLink(destination: SecondView()) {
// Text("SecondNav")
// }
// )
//This always works:
Button(action: {self.showSecondView.toggle()} ) {
Text("Call Modal Second View")
}.padding()
Text(self.showSecondView ? "true" : "false")
}.sheet(isPresented: $showSecondView) {
SecondView()
}
}
}
}
If I use a NavigationLink in the NavigationBarItems, the SecondView is displayed, but
on return to the ContentView, it crashes with the error: "Tried to pop to a view
controller that doesn't exist"
If I use a Button in the NavigationBarItems, the transition to the SecondView works
once and only once. The return to ContentView works but the button no longer functions.
Interestingly, If the first action taken is with the Button in the Body, the
NavigationBarItem does not work even once.
And the simple SecondView:
struct SecondView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
NavigationView {
VStack{
Text("This is the second view")
Button(action: { self.presentation.wrappedValue.dismiss()}) {
Text("Dismiss Modal")
}.padding()
}
}
}
}
I'm confused. Any guidance would be appreciated. Xcode 11.2 (11B44), Catalina 10.15
This is still an issue for me, I am having the same issue with popover (Modal) presentation and pushing Second controller via NavigationLink in navigationBarItems.
This is a really serious bug. The only way it works correctly is when the NavigationLink is inside NavigationView content and not navigationBarItems.
This is a really breaking as NavigationBar buttons are suppose to work that way.
I came across this issue today when I updated my Xcode to 11.2. According to this post it seems to be a bug with 13.2. I tested it on my actual iPhone X, which is still running 13.1.2, and it works just fine there.

Select SwiftUI cell in Popover

I am trying to make a popover in SwiftUI using a UIHostingController with a list that can be tapped. First, the user name and password should be filled in, and then the user role should be tapped in the list, and the popover should be dismissed when the save button is tapped.
Also, the save button in the navigation bar should be disabled until the user information has been verified.
The Xcode playground for this can be fetched from my GitHub repository https://github.com/imyrvold/Popover
To be able to use the AddUserView as a rootView in UIHostingController, I had to use an Xcode storyboard, and add it to the Resources in the Xcode Playground.
import SwiftUI
import Combine
public struct AddUserView : View {
#ObjectBinding public var loginInfo: LoginInfo
#EnvironmentObject var viewModel: RoleViewModel
#State var selectedRole: Role? = nil
#Environment(\.isPresented) var isPresented: Binding<Bool>?
public var body: some View {
NavigationView {
VStack {
TextField(self.$loginInfo.firstName, placeholder: Text("First Name"))
TextField(self.$loginInfo.lastName, placeholder: Text("Last Name"))
TextField(self.$loginInfo.email, placeholder: Text("Email"))
SecureField(self.$loginInfo.password, placeholder: Text("Password"))
Divider()
List(self.viewModel.roles) { role in
RoleCell(role: role).tapAction {
self.selectedRole = role
}
}
}
.padding()
.navigationBarTitle(Text("Add User"))
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
})//.disabled(!self.loginInfo.isValid)
}
}
// MARK:- Action methods
func saveAction() {
}
}
The first problem I have is that when I uncomment the disabled(!self.loginInfo.isValid), all the TextField's are also disabled. Not sure if that is a bug in SwiftUI?
I also want to have the rolecell set the checkmark on the cell when tapped, but so far I have been unable to figure out how to do that.
And how can I dismiss the Popover when the save button is tapped?
(When running the playground, have to click the start playground a second time to run properly, the first time the Save popover doesn't work).
Have you tried this
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
}.disabled(!self.loginInfo.isValid))

Resources