I have a long press gesture that highlights another button. The code looks like the following:
#GestureState var highlight = false
var body: some View {
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.updating($highlight) { currentstate, gestureState, transaction in
gestureState = currentstate
transaction.animation = Animation.easeInOut(duration: 2.0)
}
}
Text("highlight!")
.gesture(longPress)
Button(...) { ... }
.accentColor(self.highlight ? .green : .none)
}
How do I make sure that the transitions from .none accent to .green accent and back are more smooth? At the moment it switches rather abruptly.
The .accentColor is not animatable modifier, however you can achieve the same effect (as soon as I understood) with the following approach.
struct TestLongPressGesture: View {
#GestureState var highlight = false
var body: some View {
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.updating($highlight) { currentstate, gestureState, transaction in
transaction.animation = Animation.easeInOut(duration: 2.0)
gestureState = currentstate
}
}
return VStack {
Text("highlight!")
.gesture(longPress)
Divider()
Button("Button") { }
.font(Font.largeTitle.bold())
.foregroundColor(.white)
.colorMultiply(self.highlight ? .green : .blue)
.animation(.easeInOut, value: highlight)
}
}
}
Related
I have created a bottom alert that I want to animate when it is to be presented or removed. Currently it is not doing any animation, only showing up and removing itself when needed.
I've tried using .transition(.move(edge: .bottom)) on the actual view but no animation is shown.
How can I add a slide up/down animation for this view?
var body: some View {
ZStack {
VStack {
toolbar
Spacer()
switch viewModel.status {
case .loading:
LoadingView(isAnimating: $isLoading)
case .loaded(_):
productView
case .error(_):
Text("Please Retry.")
.onAppear {
buildBottomAlert(type: .error)
}
}
}
VStack {
Spacer()
if let bottomView = bottomAlertView {
bottomView
.transition(.move(edge: .bottom))
}
}
}
}
Bottom Alert Builder
func buildBottomAlert(type: BottomAlertType) {
self.bottomAlertView = BottomAlert(type: type)
self.showBottomAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.removeBottomAlert()
}
}
func removeBottomAlert() {
bottomAlertView = nil
showBottomAlert = false
}
For animation, you need to add your show/hide code inside body of withAnimation function.
withAnimation {
buildBottomAlert(type: .error)
}
Also, update your buildBottomAlert function like this.
func buildBottomAlert(type: BottomAlertType) {
self.bottomAlertView = BottomAlert(type: type)
self.showBottomAlert = true
Task {
//Sleep for 2 seconds
try await Task.sleep(nanoseconds: 2_000_000_000)
withAnimation {
removeBottomAlert()
}
}
}
Note: I have used sleep(nanoseconds:) instead of asyncAfter(deadline:execute:).
I have my LottieView
struct LottieView: UIViewRepresentable {
var name: String
var loopMode: LottieLoopMode = .loop
var contentMode: UIView.ContentMode = .scaleAspectFit
var paused: Bool = false
var shouldPlay: Bool = true
var animationView = AnimationView()
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
animationView.animation = Animation.named(name)
animationView.contentMode = contentMode
animationView.loopMode = loopMode
animationView.backgroundBehavior = .pauseAndRestore
if shouldPlay {
animationView.play()
}
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.heightAnchor.constraint(equalTo: view.heightAnchor),
animationView.widthAnchor.constraint(equalTo: view.widthAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
if shouldPlay {
context.coordinator.parent.animationView.play { finished in
if context.coordinator.parent.animationView.loopMode == .playOnce && finished {
context.coordinator.parent.animationView.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
context.coordinator.parent.animationView.pause()
}
}
}
} else {
context.coordinator.parent.animationView.pause()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: LottieView
init(_ parent: LottieView) {
self.parent = parent
}
}
}
which animation's start depends on it's property shouldPlay passed in view's initializer.
I use LottieView in custom AddToCartButton structure:
struct AddToCartButton: View {
let onAdd: () -> Void
#State private var shouldPresentAddAnimation: Bool = false
var body: some View {
Button {
withAnimation {
shouldPresentAddAnimation = true
onAdd()
}
} label: {
HStack(spacing: 0) {
LottieView(name: "add_to_cart_2",
loopMode: .playOnce,
contentMode: .scaleAspectFit,
shouldPlay: shouldPresentAddAnimation)
.frame(minWidth: 50, maxHeight: 40)
Text("Add to Cart")
.font(.ssButton)
.foregroundColor(.ssWhite)
.padding(.all, 10)
}
.padding(.vertical, 5)
.background {
RoundedRectangle(cornerRadius: 5)
}
.fixedSize(horizontal: true, vertical: false)
}
}
}
Clicking on this button should play the animation once and then return to initial animation state thanks to the code in LottieView's updateUIView method.
I have my main view in which there are many AddToCartButton structures created like this:
AddToCartButton {
[some code...]
}
And the effect is that at first button click, it animates properly. On second different button click the second and the first one animation fires. When clicking on third button, all three buttons fire animations.
Sample in the photos attached:
Firstly, the initial state of buttons:
Secondly, after one button click:
Finally, after second button click (two of them fire animation):
What I want is that only the button that is clicked fire it's animation.
I think the problem is that you don’t set shouldPresentAddAnimation back to false after animation is played. So every time you press the button the whole list is rendered and since you state property is true, it fires animation.
I have a common fadeIn and fadeOut animation that I use for when views appear and disappear:
struct ActiveView: View {
#State var showCode: Bool = false
#State var opacity = 0.0
var body: some View {
if self.showCode {
Color.black.opacity(0.7)
.onAppear{
let animation = Animation.easeIn(duration: 0.5)
return withAnimation(animation) {
self.opacity = 1
}
}
.onDisappear{
let animation = Animation.easeOut(duration: 0.5)
return withAnimation(animation) {
self.opacity = 0
}
}
}
}
}
However, I want to use these same animations on other views, so I want it to be simple and reusable, like this:
if self.showCode {
Color.black.opacity(0.7)
.fadeAnimation()
}
How can I achieve this?
EDIT:
Trying to implement a View extension:
extension View {
func fadeAnimation(opacity: Binding<Double>) -> some View {
self.onAppear{
let animation = Animation.easeIn(duration: 0.5)
return withAnimation(animation) {
opacity = 1
}
}
.onDisappear{
let animation = Animation.easeOut(duration: 0.5)
return withAnimation(animation) {
opacity = 0
}
}
}
}
What you try to do is already present and named opacity transition, which is written in one modifier.
Here is a demo:
struct ActiveView: View {
#State var showCode: Bool = false
var body: some View {
ZStack {
if self.showCode {
Color.black.opacity(0.7)
.transition(AnyTransition.opacity.animation(.default))
}
Button("Demo") { self.showCode.toggle() }
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
The functionality that you are trying to implement is already part of the Animation and Transition modifiers from SwiftUI.
Therefore, you can add .transition modifier to any of your Views and it will animate its insertion and removal.
if self.showCode {
Color.black.opacity(0.7)
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.5)))
}
You can use a multitude of different transitions like .slide, .scale, .offset, etc. More information about transitions here.
You can even create custom transitions with different actions for insertion and removal. In your case, different animation curves.
extension AnyTransition {
static var fadeTransition: AnyTransition {
.asymmetric(
insertion: AnyTransition.opacity.animation(.easeIn(duration: 0.5)),
removal: AnyTransition.opacity.animation(.easeOut(duration: 0.5))
)
}
}
And use it like this:
if self.showCode {
Color.black.opacity(0.7)
.transition(.fadeTransition)
}
Hope this helps 😉!
I have a TextField and some actionable elements like Button, Picker inside a view. I want to dismiss the keyboard when the use taps outside the TextField. Using the answers in this question, I achieved it. However the problem comes with other actionable items.
When I tap a Button, the action takes place but the keyboard is not dismissed. Same with a Toggle switch.
When I tap on one section of a SegmentedStyle Picker, the keyboard is dimissed but the picker selection doesn't change.
Here is my code.
struct SampleView: View {
#State var selected = 0
#State var textFieldValue = ""
var body: some View {
VStack(spacing: 16) {
TextField("Enter your name", text: $textFieldValue)
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
Picker(selection: $selected, label: Text(""), content: {
Text("Word").tag(0)
Text("Phrase").tag(1)
Text("Sentence").tag(2)
}).pickerStyle(SegmentedPickerStyle())
Button(action: {
self.textFieldValue = "button tapped"
}, label: {
Text("Tap to change text")
})
}.padding()
.onTapGesture(perform: UIApplication.dismissKeyboard)
// .gesture(TapGesture().onEnded { _ in UIApplication.dismissKeyboard()})
}
}
public extension UIApplication {
static func dismissKeyboard() {
let keyWindow = shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
As you can see in the code, I tried both options to get the tap gesture and nothing worked.
You can create an extension on View like so
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
and use it for the Views you want to dismiss the keyboard.
.onTapGesture {
self.endTextEditing()
}
I have just seen this solution in a recent raywenderlich tutorial so I assume it's currently the best solution.
Dismiss the keyboard by tapping anywhere (like others suggested) could lead to very hard to find bug (or unwanted behavior).
you loose default build-in TextField behaviors, like partial text
selection, copy, share etc.
onCommit is not called
I suggest you to think about gesture masking based on the editing state of your fields
/// Attaches `gesture` to `self` such that it has lower precedence
/// than gestures defined by `self`.
public func gesture<T>(_ gesture: T, including mask: GestureMask = .all) -> some View where T : Gesture
this help us to write
.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: (editingFlag) ? .all : .none)
Tap on the modified View will dismiss the keyboard, but only if editingFlag == true. Don't apply it on TextField! Otherwise we are on the beginning of the story again :-)
This modifier will help us to solve the trouble with Picker but not with the Button. That is easy to solve while dismiss the keyboard from its own action handler. We don't have any other controls, so we almost done
Finally we have to find the solution for rest of the View, so tap anywhere (excluding our TextFields) dismiss the keyboard. Using ZStack filled with some transparent View is probably the easiest solution.
Let see all this in action (copy - paste - run in your Xcode simulator)
import SwiftUI
struct ContentView: View {
#State var selected = 0
#State var textFieldValue0 = ""
#State var textFieldValue1 = ""
#State var editingFlag = false
#State var message = ""
var body: some View {
ZStack {
// TODO: make it Color.clear istead yellow
Color.yellow.opacity(0.1).onTapGesture {
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}
VStack {
TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
self.editingFlag = editing
}, onCommit: {
self.onCommit(txt: "salutation commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
TextField("Welcome message", text: $textFieldValue1, onEditingChanged: { editing in
self.editingFlag = editing
}, onCommit: {
self.onCommit(txt: "message commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
Picker(selection: $selected, label: Text(""), content: {
Text("Word").tag(0)
Text("Phrase").tag(1)
Text("Sentence").tag(2)
})
.pickerStyle(SegmentedPickerStyle())
.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: (editingFlag) ? .all : .none)
Button(action: {
self.textFieldValue0 = "Hi"
print("button pressed")
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}, label: {
Text("Tap to change salutation")
.padding()
.background(Color.yellow)
.cornerRadius(10)
})
Text(textFieldValue0)
Text(textFieldValue1)
Text(message).font(.largeTitle).foregroundColor(Color.red)
}
}
}
func onCommit(txt: String) {
print(txt)
self.message = [self.textFieldValue0, self.textFieldValue1].joined(separator: ", ").appending("!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you miss onCommit (it is not called while tap outside TextField), just add it to your TextField onEditingChanged (it mimics typing Return on keyboard)
TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
self.editingFlag = editing
if !editing {
self.onCommit(txt: "salutation")
}
}, onCommit: {
self.onCommit(txt: "salutation commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
I'd like to take Mark T.s Answer even further and add the entire function to an extension for View:
extension View {
func hideKeyboardWhenTappedAround() -> some View {
return self.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
}
Can then be called like:
var body: some View {
MyView()
// ...
.hideKeyboardWhenTappedAround()
// ...
}
#user3441734 is smart to enable the dismiss gesture only when needed. Rather than forcing every crevice of your forms to track state, you can:
Monitor UIWindow.keyboardWillShowNotification / willHide
Pass the current keyboard state via an EnvironmentKey set at the/a root view
Tested for iOS 14.5.
Attach dismiss gesture to the form
Form { }
.dismissKeyboardOnTap()
Setup monitor in root view
// Root view
.environment(\.keyboardIsShown, keyboardIsShown)
.onDisappear { dismantleKeyboarMonitors() }
.onAppear { setupKeyboardMonitors() }
// Monitors
#State private var keyboardIsShown = false
#State private var keyboardHideMonitor: AnyCancellable? = nil
#State private var keyboardShownMonitor: AnyCancellable? = nil
func setupKeyboardMonitors() {
keyboardShownMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillShowNotification)
.sink { _ in if !keyboardIsShown { keyboardIsShown = true } }
keyboardHideMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillHideNotification)
.sink { _ in if keyboardIsShown { keyboardIsShown = false } }
}
func dismantleKeyboarMonitors() {
keyboardHideMonitor?.cancel()
keyboardShownMonitor?.cancel()
}
SwiftUI Gesture + Sugar
struct HideKeyboardGestureModifier: ViewModifier {
#Environment(\.keyboardIsShown) var keyboardIsShown
func body(content: Content) -> some View {
content
.gesture(TapGesture().onEnded {
UIApplication.shared.resignCurrentResponder()
}, including: keyboardIsShown ? .all : .none)
}
}
extension UIApplication {
func resignCurrentResponder() {
sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
extension View {
/// Assigns a tap gesture that dismisses the first responder only when the keyboard is visible to the KeyboardIsShown EnvironmentKey
func dismissKeyboardOnTap() -> some View {
modifier(HideKeyboardGestureModifier())
}
/// Shortcut to close in a function call
func resignCurrentResponder() {
UIApplication.shared.resignCurrentResponder()
}
}
EnvironmentKey
extension EnvironmentValues {
var keyboardIsShown: Bool {
get { return self[KeyboardIsShownEVK] }
set { self[KeyboardIsShownEVK] = newValue }
}
}
private struct KeyboardIsShownEVK: EnvironmentKey {
static let defaultValue: Bool = false
}
You can set .allowsHitTesting(false) to your Picker to ignore the tap on your VStack
Apply this to root view
.onTapGesture {
UIApplication.shared.endEditing()
}
How to make border color changing animation in SwiftUI.
Here is the code with UIKit
extension UIButton{
func blink(setColor: UIColor, repeatCount: Float, duration: Double) {
self.layer.borderWidth = 1.0
let animation: CABasicAnimation = CABasicAnimation(keyPath: "borderColor")
animation.fromValue = UIColor.clear.cgColor
animation.toValue = setColor.cgColor
animation.duration = duration
animation.autoreverses = true
animation.repeatCount = repeatCount
self.layer.borderColor = UIColor.clear.cgColor
self.layer.add(animation, forKey: "")
}
}
I had a similar problem to implement a repeating text with my SwiftUI project. And the answer looks too advanced for me to implement. After some search and research. I managed to repeatedly blink my text. For someone who sees this post later, you may try this approach using withAnimation{} and .animation().
Swift 5
#State private var myRed = 0.2
#State private var myGreen = 0.2
#State private var myBlue = 0.2
var body:some View{
Button(action:{
//
}){
Text("blahblahblah")
}
.border(Color(red: myRed,green: myGreen,blue: myBlue))
.onAppear{
withAnimation{
myRed = 0.5
myGreen = 0.5
myBlue = 0
}
}
.animation(Animation.easeInOut(duration:2).repeatForever(autoreverses:true))
}
This is so much easy. First create a ViewModifier, so that we can use it easily anywhere.
import SwiftUI
struct BlinkViewModifier: ViewModifier {
let duration: Double
#State private var blinking: Bool = false
func body(content: Content) -> some View {
content
.opacity(blinking ? 0 : 1)
.animation(.easeOut(duration: duration).repeatForever())
.onAppear {
withAnimation {
blinking = true
}
}
}
}
extension View {
func blinking(duration: Double = 0.75) -> some View {
modifier(BlinkViewModifier(duration: duration))
}
}
Then use this like,
// with duration
Text("Hello, World!")
.foregroundColor(.white)
.padding()
.background(Color.blue)
.blinking(duration: 0.75) // here duration is optional. This is blinking time
// or (default is 0.75)
Text("Hello, World!")
.foregroundColor(.white)
.padding()
.background(Color.blue)
.blinking()
Update: Xcode 13.4 / iOS 15.5
A proposed solution still works with some minimal tuning.
Updated code and demo is here
Original:
Hope the following approach would be helpful. It is based on ViewModifier and can be controlled by binding. Speed of animation as well as animation kind itself can be easily changed by needs.
Note: Although there are some observed drawbacks: due to no didFinish callback provided by API for Animation it is used some trick to workaround it; also it is observed some strange handling of Animation.repeatCount, but this looks like a SwiftUI issue.
Anyway, here is a demo (screen flash at start is launch of Preview): a) activating blink in onAppear b) force activating by some action, in this case by button
struct BlinkingBorderModifier: ViewModifier {
let state: Binding<Bool>
let color: Color
let repeatCount: Int
let duration: Double
// internal wrapper is needed because there is no didFinish of Animation now
private var blinking: Binding<Bool> {
Binding<Bool>(get: {
DispatchQueue.main.asyncAfter(deadline: .now() + self.duration) {
self.state.wrappedValue = false
}
return self.state.wrappedValue }, set: {
self.state.wrappedValue = $0
})
}
func body(content: Content) -> some View
{
content
.border(self.blinking.wrappedValue ? self.color : Color.clear, width: 1.0)
.animation( // Kind of animation can be changed per needs
Animation.linear(duration:self.duration).repeatCount(self.repeatCount)
)
}
}
extension View {
func blinkBorder(on state: Binding<Bool>, color: Color,
repeatCount: Int = 1, duration: Double = 0.5) -> some View {
self.modifier(BlinkingBorderModifier(state: state, color: color,
repeatCount: repeatCount, duration: duration))
}
}
struct TestBlinkingBorder: View {
#State var blink = false
var body: some View {
VStack {
Button(action: { self.blink = true }) {
Text("Force Blinking")
}
Divider()
Text("Hello, World!").padding()
.blinkBorder(on: $blink, color: Color.red, repeatCount: 5, duration: 0.5)
}
.onAppear {
self.blink = true
}
}
}
After a lot of research on this topic, I found two ways to solve this thing. Each has its advantages and disadvantages.
The Animation way
There is a direct answer to your question. It's not elegant as it relies on you putting in the timing in a redundant way.
Add a reverse function to Animation like this:
extension Animation {
func reverse(on: Binding<Bool>, delay: Double) -> Self {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
on.wrappedValue = false /// Switch off after `delay` time
}
return self
}
}
With this extension, you can create a text, that scales up and back again after a button was pressed like this:
struct BlinkingText: View {
#State private var isBlinking: Bool = false
var body: some View {
VStack {
Button {
isBlinking = true
} label: {
Text("Let it blink")
}
.padding()
Text("Blink!")
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(isBlinking ? 2.0 : 1.0)
.animation(Animation.easeInOut(duration: 0.5).reverse(on: $isBlinking, delay: 0.5))
}
}
}
It's not perfect so I did more research.
The Transition way
Actually, SwiftUI provides two ways to get from one look (visual representation, ... you name it) to another smoothly.
Animations are especially designed to get from one View to another look of the same View.(same = same struct, different instance)
Transitions are made to get from one view to another view by transitioning out the old View and transition in another one.
So, here's another code snippet using transitions. The hacky part is the if-else which ensures, that one View disappears and another one appears.
struct LetItBlink: View {
#State var count: Int
var body: some View {
VStack {
Button {
count += 1
} label: {
Text("Let it blink: \(count)")
}
.padding()
if count % 2 == 0 {
BlinkingText(text: "Blink Blink 1!")
} else {
BlinkingText(text: "Blink Blink 2!")
}
}
.animation(.default)
}
}
private struct BlinkingText: View {
let text: String
var body: some View {
Text(text)
.foregroundColor(.red)
.font(.largeTitle)
.padding()
.transition(AnyTransition.scale(scale: 1.5).combined(with: .opacity))
}
}
You can create nice and interesting "animations" by combining transitions.
What's my personal opinion?
Both are not perfectly elegant and look somehow "hacky". Either because of the delay management or because of the if-else. Adding the possibility to SwiftUI to chain Animations would help.
Transitions look more customisable, but this depends on the actual requirement.
Both are necessary. Adding a default animation is one of the first things I do, because they make the app look and feel smooth.
This is some code I came up with for a blinking button in SwiftUI 2, it might help someone. It's a toggle button that blinks a capsule shaped overlay around the button. It works but personally, I don't like my function blink() that calls itself.
struct BlinkingButton:View{
#Binding var val:Bool
var label:String
#State private var blinkState:Bool = false
var body: some View{
Button(label){
val.toggle()
if val{
blink()
}
}
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.foregroundColor(.white)
.background(val ? Color.blue:Color.gray)
.clipShape(Capsule())
.padding(.all,8)
.overlay(Capsule().stroke( blinkState && val ? Color.red:Color.clear,lineWidth: 3))
}
func blink(){
blinkState.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
if val{
blink()
}
}
}
}
In use it looks like this:
struct ContentView: View {
#State private var togVal:Bool = false
var body: some View {
VStack{
Text("\(togVal ? "ON":"OFF")")
BlinkingButton(val: $togVal, label: "tap me")
}
}
}