рд╡рд┐рдЪрд╛рд░
SwiftUI рдХреЛ WWDC 2019 рдореЗрдВ рдкреЗрд╢ рдХрд┐рдпрд╛ рдЧрдпрд╛ рдерд╛, рдЬреЛ рдПрдХ рдРрд╕реА рддрдХрдиреАрдХ рд╣реИ рдЬреЛ Apple рдкрд╛рд░рд┐рд╕реНрдерд┐рддрд┐рдХреА рддрдВрддреНрд░ рдХреЗ рд▓рд┐рдП рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛрдВ рдореЗрдВ UI рдХреЗ рдирд┐рд░реНрдорд╛рдг рдХреЛ рдореМрд▓рд┐рдХ рд░реВрдк рд╕реЗ рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░рддреА рд╣реИред рд╣рдо рдбрд┐рд╕реНрдЯрд┐рд▓рд░реА рдореЗрдВ рдпрд╣ рд╕рдордЭрдиреЗ рдореЗрдВ рд░реБрдЪрд┐ рд░рдЦрддреЗ рд╣реИрдВ рдХрд┐ рдЗрд╕реЗ рдПрдкреНрдкрд▓ рдХреЗ рдЙрджрд╛рд╣рд░рдгреЛрдВ рдореЗрдВ рдкреНрд░рд╕реНрддреБрдд рдХрд░рдиреЗ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рдереЛрдбрд╝рд╛ рдЧрд╣рд░рд╛ рд╣реИред рдЖрджрд░реНрд╢ рд░реВрдк рд╕реЗ, рдЖрдИрдУрдПрд╕ рдЯреАрдо рдФрд░ рдпреВрдЖрдИ рд╕рдореБрджрд╛рдп рдХреЗ рд▓рд┐рдП рдХреБрдЫ рдЙрдкрдпреЛрдЧреА рдШрдЯрдХ рдХреЛ рдкрдХрдбрд╝рдирд╛ рдЖрд╡рд╢реНрдпрдХ рдерд╛ред рдпрд╣ рдЗрд╕ рдкрд░ рд╡рд┐рдЪрд╛рд░реЛрдВ рдХреЗ рд╕рд╛рде рдХрдбрд╝рд╛ рд╣реЛ рдЧрдпрд╛, рдЗрд╕рд▓рд┐рдП рд╣рдордиреЗ рдХреБрдЫ рдордЬреЗрджрд╛рд░ рддрд░реАрдХреЗ рд╕реЗ рдХрдЯреМрддреА рдХрд░рдиреЗ рдХрд╛ рдлреИрд╕рд▓рд╛ рдХрд┐рдпрд╛ред рдпрд╣ рдЕрд╡рдзрд╛рд░рдгрд╛ рдЗрд╕рд╕реЗ рдкреНрд░реЗрд░рд┐рдд рдереА :

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