我创建了一个“通知横幅”,7.5 秒后,转到下一个通知。在右侧,我有一些项目符号,以便您可以在收到 5 条通知中的第 2 条通知时看到。
我想要实现的是,当计时器运行时,慢慢地将气泡从灰色填充到白色。因此,每个气泡都会慢慢填充,然后移动到下一个气泡,依此类推。目前正在像这样逐渐填充(请参见下面的视频)。
struct SoleInAppNotifications: View {
@State private var currentIndex: Int = 0
@State private var isPaused: Bool = false
@State var hasImage: Bool? = true
let timer = Timer.publish(every: 7.5, on: .main, in: .common).autoconnect()
var body: some View {
HStack(spacing: .zero) {
if hasImage ?? false {
Image("raiff")
.padding(.trailing, 17)
}
VStack(alignment: .leading, spacing: .zero) {
Text("Notification #1")
.foregroundColor(.white)
.lineLimit(1)
.padding(.bottom, 1)
Text("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, se")
.foregroundColor(.white)
.lineLimit(2)
}
VStack(spacing: .zero) {
ForEach(0..<5, id: \.self) { index in
BubbleView(isActive: index == currentIndex, counter: currentIndex, countTo: 5)
}
}
}
.frame(maxWidth: .infinity)
.padding(17)
.background(Color.black)
.onReceive(timer) { _ in
if !isPaused, currentIndex < 5 {
withAnimation {
currentIndex = (currentIndex + 1)
}
}
}
}
}
struct BubbleView: View {
let isActive: Bool
var counter: Int
var countTo: Int
var body: some View {
ZStack {
if isActive {
RoundedRectangle(cornerRadius: 3)
.fill(Color.gray)
.frame(width: 6, height: 24)
.overlay(
RoundedRectangle(cornerRadius: 3).trim(from:0, to: progress())
.foregroundColor(
(completed() ? Color.gray : Color.white)
)
).animation(
.easeInOut(duration: 0.2)
)
} else {
Circle()
.fill(Color.gray)
.frame(width: 6, height: 6)
.animation(.easeInOut)
.padding(4)
}
}
}
func completed() -> Bool {
return progress() == 1
}
func progress() -> CGFloat {
return (CGFloat(counter) / CGFloat(countTo))
}
}
要实现加载动画,进度应始终从 0 开始并以 1 结束。
在进度函数中,您根据当前索引和计数设置进度。这导致最后一张幻灯片从 100% 开始,据我了解,这不是预期的结果。
要在每个气泡上实现完整的动画,您需要根据静态值(您希望实现的速度)计算加载时间。例如。 7.5 秒(满分 100)(根据需要更改此值)。
struct SoleInAppNotifications: View {
//...
@State private var progress: CGFloat = 0
let timer = Timer.publish(every: 7.5/100,
on: .main,
in: .common).autoconnect()
//...
var body: some View {
//...
BubbleView(progress: $progress,
isActive: index == currentIndex,
counter: currentIndex,
countTo: 5)
//...
.onReceive(timer) { _ in
withAnimation {
if progress < 1 {
progress += 1/100
} else if !isPaused, currentIndex < 5 {
currentIndex = (currentIndex + 1)
progress = 0
}
}
}
}
}
struct BubbleView: View {
@Binding var progress: CGFloat
//...
/* Remove this function
func progress() -> CGFloat {
return (CGFloat(counter) / CGFloat(countTo))
}*/
}
(虽然是基本实现,但仍然可以进行一些改进)
class Bubble {
var counter: CGFloat
var countTo: CGFloat
var i: CGFloat
var isActive: Bool
var progress: CGFloat
init(counter: CGFloat,
countTo: CGFloat,
i: CGFloat,
isActive: Bool,
progress: CGFloat) {
self.counter = counter
self.countTo = countTo
self.i = i
self.isActive = isActive
self.progress = progress
}
}
struct SoleInAppNotifications: View {
@StateObject private var vm = ViewModel()
let animationTimer = Timer.publish(every: 7.5/100,
on: .main,
in: .common).autoconnect()
var body: some View {
HStack(spacing: .zero) {
Image("raiff")
.padding(.trailing, 17)
VStack(alignment: .leading, spacing: .zero) {
Text("Notification #1")
.foregroundColor(.white)
.lineLimit(1)
.padding(.bottom, 1)
Text("Notification body")
.foregroundColor(.white)
.lineLimit(2)
}
VStack(spacing: .zero) {
ForEach(vm.bubbles.indices, id: \.self) { i in
BubbleView(bubble: $vm.bubbles[i])
.onChange(of: vm.currentIndex) { _, _ in
vm.bubbles[i].isActive = CGFloat(i) == vm.currentIndex
}
.onChange(of: vm.progress) { _, _ in
print(vm.progress)
vm.bubbles[i].progress = vm.progress
}
}
}
}
.frame(maxWidth: .infinity)
.padding(17)
.background(Color.black)
.onReceive(animationTimer){ _ in
withAnimation {
if vm.progress < 1 {
vm.progress += 1/100
} else if vm.currentIndex < 5 {
withAnimation {
vm.currentIndex += 1
vm.progress = 0
}
}
}
}
}
}
extension SoleInAppNotifications {
class ViewModel: ObservableObject {
@Published var bubbles: [Bubble]
@Published var currentIndex: CGFloat = 0
@Published var progress: CGFloat = 0
init() {
self.bubbles = []
for i in 0..<5 {
bubbles.append(Bubble(counter: currentIndex,
countTo: 5,
i: CGFloat(i),
isActive: i == 0 ? true : false,
progress: progress))
}
}
}
}
struct BubbleView: View {
@Binding var bubble: Bubble
var body: some View {
if bubble.isActive {
RoundedRectangle(cornerRadius: 3)
.fill(Color.gray)
.frame(width: 6, height: 24)
.overlay(
RoundedRectangle(cornerRadius: 3)
.trim(from:0,
to: bubble.progress)
.foregroundColor(
(completed ? Color.gray : Color.white)
)
).animation(
.easeInOut(duration: 0.2)
)
} else {
Circle()
.fill(Color.gray)
.frame(width: 6, height: 6)
.animation(.easeInOut)
.padding(4)
}
}
var completed: Bool {
bubble.progress == 1
}
}