我需要的样品:
由于.onAnimationCompleted { // Some work... }
的缺失,这确实是个问题。
通常,我需要具有以下特征的解决方案:
我的代码:
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
}
}
}
答案 0 :(得分:4)
这个怎么样?不错的呼叫站点,逻辑封装在主视图之外,可选的闪烁持续时间。您只需要提供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)
}
}
}
这是您要调用的视图扩展
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))
}
}
这实际上是发生魔法的ViewModifier
// 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
}
}
}
}
}
}
答案 1 :(得分:0)
John的回答绝对不错,可以帮助我准确找到所需的内容。我扩展了答案,以允许将任何视图修改一次都“刷新”并返回。
示例结果:
示例代码:
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()
}
}
}
}
这是约翰回答中的修改代码。
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
}
}
}
}
}