рд╕реНрд╡рд┐рдлреНрдЯрдпреВрдЖрдИ рдореЗрдВ рдирд┐рд░реНрд╡рд┐рд╡рд╛рдж рдПрдирд┐рдореЗрд╢рди

рд╡рд┐рдЪрд╛рд░


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.


.


All Articles