Idéia
O SwiftUI foi introduzido na WWDC 2019, uma tecnologia que afeta fundamentalmente a criação de UIs em aplicativos para o ecossistema da Apple. Na Distillery, ficamos interessados em entendê-lo um pouco mais do que o que é apresentado nos exemplos da Apple. Idealmente, era necessário amordaçar algum componente útil para a equipe do iOS e a comunidade da interface do usuário. Acabou sendo apertado com idéias sobre isso, então decidimos cortar algo engraçado. Este conceito foi inspirado em :

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