gpt4 book ai didi

SwiftUI: how to play ping-pong animation once? Correct way to play animation forward and backward?(SwiftUI:如何播放一次乒乓球动画?正确的方法是向前和向后播放动画?)

转载 作者:bug小助手 更新时间:2023-10-28 11:30:37 24 4
gpt4 key购买 nike



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:

通常,我需要具有以下特点的解决方案:




  1. Most short and elegant way of playing some ping-pong animation ONCE. Not infinite!

  2. Make code reusable. As example - made it as ViewModifier.

  3. 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
}
}
}

更多回答

Maybe this will help: talk.objc.io/episodes/S01E173-building-a-shake-animation

也许这会有帮助:talk.objc.io/episodes/S01E173-building-a-shake-animation

优秀答案推荐

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()。



Blink demo



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:

示例结果:


enter image description here


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动画的混合体。谢谢!

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com