Idea
SwiftUI se presentó en WWDC 2019, una tecnología que afecta fundamentalmente la creación de IU en aplicaciones para el ecosistema de Apple. En Distillery nos interesamos en entenderlo un poco más de lo que se presenta en los ejemplos de Apple. Idealmente, era necesario amordazar algún componente útil para el equipo de iOS y la comunidad de la interfaz de usuario. Resultó ser estrecho con ideas sobre esto, así que decidimos cortar algo simplemente divertido. Este concepto fue inspirado por :

. , , 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.
.