Reputation: 174
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: "")
}
}
Upvotes: 10
Views: 13965
Reputation: 257583
A proposed solution still works with some minimal tuning.
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
}
}
}
Upvotes: 3
Reputation: 1356
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()
Upvotes: 10
Reputation: 43
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")
}
}
}
Upvotes: 0
Reputation: 11892
After a lot of research on this topic, I found two ways to solve this thing. Each has its advantages and disadvantages.
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.
Actually, SwiftUI provides two ways to get from one look (visual representation, ... you name it) to another smoothly.
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.
if-else
. Adding the possibility to SwiftUI to chain Animations would help.Upvotes: 2
Reputation: 475
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))
}
Upvotes: 7