Sample of what I need:
我需要的样品:
.
。
As there is absent .onAnimationCompleted { // Some work... }
its pretty problematic.
由于没有.onAnimationComplete{//一些工作...}这是个很大的问题。
Generally I need the solution that will have a following characteristics:
通常,我需要具有以下特点的解决方案:
- Most short and elegant way of playing some ping-pong animation ONCE. Not infinite!
- Make code reusable. As example - made it as ViewModifier.
- To have a way to call animation externally
my code:
我的代码是:
import SwiftUI
import Combine
struct ContentView: View {
@State var descr: String = ""
@State var onError = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
BlurredTextField(title: "Description", text: $descr, onError: $onError)
Button("Commit") {
if self.descr.isEmpty {
self.onError.send()
}
}
}
}
}
struct BlurredTextField: View {
let title: String
@Binding var text: String
@Binding var onError: PassthroughSubject<Void, Never>
@State private var anim: Bool = false
@State private var timer: Timer?
@State private var cancellables: Set<AnyCancellable> = Set()
private let animationDiration: Double = 1
var body: some View {
TextField(title, text: $text)
.blur(radius: anim ? 10 : 0)
.animation(.easeInOut(duration: animationDiration))
.onAppear {
self.onError
.sink(receiveValue: self.toggleError)
.store(in: &self.cancellables)
}
}
func toggleError() {
timer?.invalidate()// no blinking hack
anim = true
timer = Timer.scheduledTimer(withTimeInterval: animationDiration, repeats: false) { _ in
self.anim = false
}
}
}
更多回答
How about this? Nice call site, logic encapsulated away from your main view, optional blink duration. All you need to provide is the PassthroughSubject
, and call .send()
when you want the blink to happen.
这个怎么样?漂亮的调用站点,远离主视图的逻辑封装,可选的闪烁时间。您只需提供PassthroughSubject,并在希望闪烁时调用.Send()。
import SwiftUI
import Combine
struct ContentView: View {
let blinkPublisher = PassthroughSubject<Void, Never>()
var body: some View {
VStack(spacing: 10) {
Button("Blink") {
self.blinkPublisher.send()
}
Text("Hi")
.addOpacityBlinker(subscribedTo: blinkPublisher)
Text("Hi")
.addOpacityBlinker(subscribedTo: blinkPublisher, duration: 0.5)
}
}
}
Here's the view extension you would call
下面是您将调用的视图扩展
extension View {
// the generic constraints here tell the compiler to accept any publisher
// that sends outputs no value and never errors
// this could be a PassthroughSubject like above, or we could even set up a TimerPublisher
// that publishes on an interval, if we wanted a looping animation
// (we'd have to map it's output to Void first)
func addOpacityBlinker<T: Publisher>(subscribedTo publisher: T, duration: Double = 1)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.modifier(OpacityBlinker(subscribedTo: publisher.eraseToAnyPublisher(),
duration: duration))
}
}
Here's the ViewModifier
where the magic actually happens
以下是实际发生魔术的ViewModiator
// you could call the .modifier(OpacityBlinker(...)) on your view directly,
// but I like the View extension method, as it just feels cleaner to me
struct OpacityBlinker: ViewModifier {
// this is just here to switch on and off, animating the blur on and off
@State private var isBlurred = false
var publisher: AnyPublisher<Void, Never>
// The total time it takes to blur and unblur
var duration: Double
// this initializer is not necessary, but allows us to specify a default value for duration,
// and the call side looks nicer with the 'subscribedTo' label
init(subscribedTo publisher: AnyPublisher<Void, Never>, duration: Double = 1) {
self.publisher = publisher
self.duration = duration
}
func body(content: Content) -> some View {
content
.blur(radius: isBlurred ? 10 : 0)
// This basically subscribes to the publisher, and triggers the closure
// whenever the publisher fires
.onReceive(publisher) { _ in
// perform the first half of the animation by changing isBlurred to true
// this takes place over half the duration
withAnimation(.linear(duration: self.duration / 2)) {
self.isBlurred = true
// schedule isBlurred to return to false after half the duration
// this means that the end state will return to an unblurred view
DispatchQueue.main.asyncAfter(deadline: .now() + self.duration / 2) {
withAnimation(.linear(duration: self.duration / 2)) {
self.isBlurred = false
}
}
}
}
}
}
We can use animation chaining just create second animation right after first but with delay of first animation duration.
我们可以使用动画链接,只是在第一个动画之后创建第二个动画,但要延迟第一个动画的持续时间。
struct DynamicTextView: View {
@ObservedObject
var dynamicText: DynamicText
var body: some View {
Text(dynamicText.text)
.scaleEffect(dynamicText.scale)
}
}
final class DynamicText: ObservableObject {
@Published
var text: String = ""
@Published
fileprivate var scale: CGFloat = 1
func makeView() -> DynamicTextView {
DynamicTextView(dynamicText: self)
}
func animateScale(text: String, maxScale: CGFloat = 1.3, duration: CGFloat = 0.35) {
self.text = text
withAnimation(Animation.easeIn(duration: duration)) { [weak self] in
self?.scale = maxScale
}
withAnimation(Animation.easeOut(duration: duration).delay(duration)) { [weak self] in
self?.scale = 1.0
}
}
}
John's answer is absolutely great and helped me get to exactly what I was looking for. I extended the answer to allow for any view modification to "flash" once and return.
约翰的回答绝对很棒,帮助我找到了我想要的东西。我扩展了答案,允许对“Flash”进行任何视图修改,只需一次即可返回。
Example Result:
示例结果:
Example Code:
示例代码:
struct FlashTestView : View {
let flashPublisher1 = PassthroughSubject<Void, Never>()
let flashPublisher2 = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Text("Scale Out & In")
.padding(20)
.background(Color.white)
.flash(on: flashPublisher1) { (view, isFlashing) in
view
.scaleEffect(isFlashing ? 1.5 : 1)
}
.onTapGesture {
flashPublisher1.send()
}
Divider()
Text("Flash Text & Background")
.padding(20)
// Connivence view extension for background and text color
.flash(
on: flashPublisher2,
originalBackgroundColor: .white,
flashBackgroundColor: .blue,
originalForegroundColor: .primary,
flashForegroundColor: .white)
.onTapGesture {
flashPublisher2.send()
}
}
}
}
Here's the modified code from John's answer.
以下是John答案中修改后的代码。
extension View {
/// Listens to a signal from a publisher and temporarily applies styles via the content callback.
/// - Parameters:
/// - publisher: The publisher that sends a signal to apply the temp styles.
/// - animation: The animation used to change properties.
/// - delayBack: How long, in seconds, after flashing starts should the styles start to revert. Typically this is the same duration as the animation.
/// - content: A closure with two arguments to allow customizing the view when flashing. Should return the modified view back out.
/// - view: The view being modified.
/// - isFlashing: A boolean to indicate if a flash should be applied. Example: `view.scaleEffect(isFlashing ? 1.5 : 1)`
/// - Returns: A view that applies its flash changes when it receives its signal.
func flash<T: Publisher, InnerContent: View>(
on publisher: T,
animation: Animation = .easeInOut(duration: 0.3),
delayBack: Double = 0.3,
@ViewBuilder content: @escaping (_ view: Self, _ isFlashing: Bool) -> InnerContent)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.modifier(
FlashStyleModifier(
publisher: publisher.eraseToAnyPublisher(),
animation: animation,
delayBack: delayBack,
content: { (view, isFlashing) in
return content(self, isFlashing)
}))
}
/// A helper function built on top of the method above.
/// Listens to a signal from a publisher and temporarily animates to a background color and text color.
/// - Parameters:
/// - publisher: The publisher that sends a signal to apply the temp styles.
/// - animation: The animation used to change properties.
/// - delayBack: How long, in seconds, after flashing starts should the styles start to revert. Typically this is the same duration as the animation.
/// - originalBackgroundColor: The normal state background color
/// - flashBackgroundColor: The background color when flashing.
/// - originalForegroundColor: The normal text color.
/// - flashForegroundColor: The text color when flashing.
/// - Returns: A view that flashes it's background and text color.
func flash<T: Publisher>(
on publisher: T,
animation: Animation = .easeInOut(duration: 0.3),
delayBack: Double = 0.3,
originalBackgroundColor: Color,
flashBackgroundColor: Color,
originalForegroundColor: Color,
flashForegroundColor: Color)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.flash(on: publisher, animation: animation) { view, isFlashing in
return view
// Need to apply arbitrary foreground color, but it's not animatable but need for colorMultiply to work.
.foregroundColor(.white)
// colorMultiply is animatable, so make foregroundColor flash happen here
.colorMultiply(isFlashing ? flashForegroundColor : originalForegroundColor)
// Apply background AFTER colorMultiply so that background color is not unexpectedly modified
.background(isFlashing ? flashBackgroundColor : originalBackgroundColor)
}
}
}
/// A view modifier that temporarily applies styles based on a signal from a publisher.
struct FlashStyleModifier<InnerContent: View>: ViewModifier {
@State
private var isFlashing = false
let publisher: AnyPublisher<Void, Never>
let animation: Animation
let delayBack: Double
let content: (_ view: Content, _ isFlashing: Bool) -> InnerContent
func body(content: Content) -> some View {
self.content(content, isFlashing)
.onReceive(publisher) { _ in
withAnimation(animation) {
self.isFlashing = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + delayBack) {
withAnimation(animation) {
self.isFlashing = false
}
}
}
}
}
更多回答
Looks like an awesome solution! Thanks a lot!
看起来是个很棒的解决方案!非常感谢!
@Andrew Here's some comments :D
@安德鲁以下是一些评论:
This is beautiful for the neophyte: mixture of Combine & SwiftUI animation. Thanks!
这对于新手来说很漂亮:组合和SwiftUI动画的混合体。谢谢!
我是一名优秀的程序员,十分优秀!