فكرة
تم تقديم SwiftUI في WWDC 2019 ، وهي تقنية تؤثر بشكل أساسي على إنشاء واجهات المستخدم في التطبيقات لنظام Apple البيئي. أصبحنا في Distillery مهتمين بفهمها بشكل أعمق قليلاً مما هو معروض في أمثلة Apple. من الناحية المثالية ، كان من الضروري الحصول على بعض المكونات المفيدة لفريق iOS ومجتمع واجهة المستخدم. اتضح أنه كان مشدودًا بالأفكار حول هذا ، لذلك قررنا قطع شيء مضحك. هذا المفهوم مستوحى من :

. , , SwiftUI - , UI WWDC 2019.
, :

CocoaPods.
Example . ExampleView.swift RainbowBar:
RainbowBar(waveEmitPeriod: 0.3,
visibleWavesCount: 3,
waveColors: [.red, .green, .blue],
backgroundColor: .white,
animated: animatedSignal) {
self.running = false
}
, , RainbowBar.
:
waveEmitPeriod
— , .
visibleWavesCount
— , .
waveColors
— , . . .
backgroundColor
— . , ) , , .
animated
— Combine- PassthroughSubject<Bool, Never>()
completion
. .
, , , , . , — . DeviceKit. , , .
WavesView. Y .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0), anchor: .center)
. Spacer centerSpacing. HStack height:
public var body: some View {
HStack {
WavesView(waveEmitPeriod: waveEmitPeriod,
visibleWavesCount: visibleWavesCount,
waveColors: waveColors,
backgroundColor: backgroundColor,
topCornerRadius: waveTopCornerRadius,
bottomCornerRadius: waveBottomCornerRadius,
animatedSignal: animated,
completion: completion)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0), anchor: .center)
Spacer().frame(width: centerSpacing)
WavesView(waveEmitPeriod: waveEmitPeriod,
visibleWavesCount: visibleWavesCount,
waveColors: waveColors,
backgroundColor: backgroundColor,
topCornerRadius: waveTopCornerRadius,
bottomCornerRadius: waveBottomCornerRadius,
animatedSignal: animated,
completion: nil)
}.frame(height: height)
}
WavesView ZStack` WaveView
ZStack {
ForEach(waveNodes) { node in
WaveView(animationDuration: self.animationDuration,
animationFinished: self.waveFinished,
node: node,
topCornerRadius: self.topCornerRadius,
bottomCornerRadius: self.bottomCornerRadius)
}
}
@State private var waveNodes = [WaveNode]()
. / animatedInnerState:
@State private var animatedInnerState: Bool = false {
didSet {
if animatedInnerState {
var res = [NotchWaveNode]()
for index in 0..<visibleWavesCount {
guard let color = self.colorEmitter.nextColor(from: self.waveColors) else { continue }
let newNode = NotchWaveNode(color: color,
delay: waveEmitPeriod * Double(index))
res.append(newNode)
}
waveNodes = res
} else {
waveNodes.removeAll {
!$0.started
}
if let lastVisibleNode = waveNodes.last as? NotchWaveNode {
let gradientNode = GradientWaveNode(frontColor: lastVisibleNode.color,
backColor: backgroundColor,
animationDuration: animationDuration,
delay: 0,
animationFinished: self.waveFinished)
waveNodes.append(gradientNode)
}
}
}
}
:
onReceive(waveFinished) { node in
if node is GradientWaveNode, let completion = self.completion {
DispatchQueue.main.async {
completion()
}
return
}
// remove invisible (lower, first) node?
if self.waveNodes.count > 0 {
var removeFirstNode = false
if self.waveNodes.count > 1 {
removeFirstNode = self.waveNodes[1].finished
}
if removeFirstNode {
self.waveNodes.removeFirst()
}
}
//add new color (node)
if self.animatedInnerState, let color = self.colorEmitter.nextColor(from: self.waveColors) {
let newNode = NotchWaveNode(color: color, delay: 0)
self.waveNodes.append(newNode)
}
}
() . SwiftUI — . body , , @State
.
visibleWavesCount (node.delay), . , , . drawingGroup, ZStack — .
WaveView, :
func makeWave(from node: WaveNode) -> some View {
let phase: CGFloat = self.animated ? 1.0 : 0.0
if let notchNode = node as? NotchWaveNode {
return AnyView(NotchWave(phase: phase,
animationFinished: self.animationFinished,
node: notchNode,
topCornerRadius: topCornerRadius,
bottomCornerRadius: bottomCornerRadius).foregroundColor(notchNode.color))
} else if let gradientNode = node as? GradientWaveNode {
return AnyView(GradientWave(phase: phase,
frontColor: gradientNode.frontColor,
backColor: gradientNode.backColor,
node: gradientNode,
minWidth: topCornerRadius + bottomCornerRadius))
} else {
return AnyView(EmptyView())
}
}
var body: some View {
return makeWave(from: node).animation(Animation.easeIn(duration: animationDuration).delay(node.delay)).onAppear {
self.animated.toggle()
}
}
" " (NotchWave, GradientWave) , , . — . easeIn .
, NotchWave — ( View), Shape — .
struct NotchWave: Shape {
var phase: CGFloat
var animationFinished: AnimationSignal
var node: NotchWaveNode
var topCornerRadius, bottomCornerRadius: CGFloat
var animatableData: CGFloat {
get { return phase }
set { phase = newValue }
}
func path(in rect: CGRect) -> Path {
if !self.node.started && self.phase > 0.0 {
self.node.started = true
}
DispatchQueue.main.async {
if self.phase >= 1.0 {
self.node.finished = true
self.animationFinished.send(self.node)
}
}
var p = Path()
p.move(to: CGPoint.zero)
let currentWidth = 2 * (topCornerRadius + bottomCornerRadius) + rect.size.width * phase
p.addLine(to: CGPoint(x: currentWidth, y: 0))
let topArcCenter = CGPoint(x: currentWidth, y: topCornerRadius)
p.addArc(center: topArcCenter, radius: topCornerRadius, startAngle: .degrees(270), endAngle: .degrees(180), clockwise: true)
let height = rect.size.height
p.addLine(to: CGPoint(x: currentWidth - topCornerRadius, y: height - bottomCornerRadius))
let bottomArcCenter = CGPoint(x: currentWidth - topCornerRadius - bottomCornerRadius, y: height - bottomCornerRadius)
p.addArc(center: bottomArcCenter, radius: bottomCornerRadius, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: false)
p.addLine(to: CGPoint(x: 0, y: height))
p.closeSubpath()
return p
}
}
animatableData phase, . @State
, , .
GCD , UI, ( ) path. , SwiftUI , completion animateWithDuration UIKit:
DispatchQueue.main.async {
if self.phase >= 1.0 {
self.node.finished = true
self.animationFinished.send(self.node)
}
}
. — View:
struct GradientWave: View {
var phase: CGFloat
var frontColor, backColor: Color
var node: GradientWaveNode
var minWidth: CGFloat
var body: some View {
if self.phase == 0 {
node.startAnimationTimer()
}
return GeometryReader { geometry in
HStack(spacing: 0) {
Rectangle().foregroundColor(self.backColor).frame(width: (geometry.size.width + self.minWidth) * self.phase)
Rectangle().fill(LinearGradient(gradient: Gradient(colors: [self.backColor, self.frontColor]), startPoint: .leading, endPoint: .trailing)).frame(width: self.minWidth)
Spacer()
}
}
}
}
: backgroundColor, backgroundColor NotchWave. path(in rect: CGRect) -> Path
NotchWave GeometryReader .
NotchWave , var body: some View
phase. startAnimationTimer()
GradientWaveNode
:
class GradientWaveNode: WaveNode {
let frontColor, backColor: Color
let animationDuration: Double
let animationFinished: AnimationSignal
private var timer: Timer?
func startAnimationTimer() {
self.timer = Timer.scheduledTimer(withTimeInterval: animationDuration, repeats: false) { _ in
self.animationFinished.send(self)
}
}
init(frontColor: Color, backColor: Color, animationDuration: Double, delay: Double, animationFinished: AnimationSignal) {
self.frontColor = frontColor
self.backColor = backColor
self.animationDuration = animationDuration
self.animationFinished = animationFinished
super.init(delay: delay)
}
deinit {
timer?.invalidate()
}
}
, . . , , SwiftUI (
:
class NotchWaveNode: WaveNode {
let color: Color
init(color: Color, delay: Double) {
self.color = color
super.init(delay: delay)
}
}
:
class WaveNode: Identifiable {
let id = UUID()
let delay: Double
var started: Bool = false
var finished: Bool = false
init(delay: Double) {
self.delay = delay
}
}
Identifiable, ForEach. ForEach id: \.self
. , , Hashable. Identifiable .
ColorEmitter, . , SwiftUI State body:
class ColorEmitter {
var colors, refColors: [Color]?
func nextColor(from newColors: [Color]) -> Color? {
if !(refColors?.elementsEqual(newColors) ?? false) {
colors = newColors
refColors = newColors
}
let res = colors?.removeFirst()
if let res = res {
colors?.append(res)
}
return res
}
}
, SwiftUI . Combine rx-. , Interface builder: UI . dark mode. SwiftUI . . ( Combine) .
.
CocoaPods.
.