Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix animation glitch on iOS 17 #12

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 17 additions & 27 deletions Sources/Liquid/PrivateViews/LiquidCircleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,31 @@
//

import SwiftUI
import Combine

struct LiquidCircleView: View {
@State var samples: Int
@State var radians: AnimatableArray
@State var trigger: Timer.TimerPublisher?
@State var cancellable: Cancellable?
@State var animate: Bool
let period: TimeInterval

init(samples: Int, period: TimeInterval) {
init(samples: Int, period: TimeInterval, animate: Bool) {
self._samples = .init(initialValue: samples)
self._radians = .init(initialValue: AnimatableArray(LiquidCircleView.generateRadial(samples)))
self.period = period
self._animate = .init(initialValue: animate)
}

var body: some View {
LiquidCircle(radians: radians)
.onAppear {
startTimer()
animatedRadianUpdate()
}
.onDisappear {
stopTimer()
animate = false
}
}

static func generateRadial(_ count: Int = 6) -> [Double] {

var radians: [Double] = []
let offset = Double.random(in: 0...(.pi / Double(count)))
for i in 0..<count {
Expand All @@ -44,30 +42,22 @@ struct LiquidCircleView: View {
return radians
}

private func startTimer() {
guard cancellable == nil else { return }

// Get the animation started immediately by updating the radians.
DispatchQueue.main.asyncAfter(deadline: .now()) {
animatedRadianUpdate()
}

// Periodically update the radians to continue the animation.
cancellable = Timer.publish(every: period, on: .main, in: .common)
.autoconnect()
.sink { _ in
animatedRadianUpdate()
func animatedRadianUpdate() {
guard animate else { return }

if #available(iOS 17.0, *) {
withAnimation(.linear(duration: period)) {
radians = AnimatableArray(LiquidCircleView.generateRadial(samples))
} completion: {
self.animatedRadianUpdate()
}

func animatedRadianUpdate() {
} else {
withAnimation(.linear(duration: period)) {
radians = AnimatableArray(LiquidCircleView.generateRadial(samples))
}
Task.delayed(by: .seconds(period)) { @MainActor in
self.animatedRadianUpdate()
}
}
}

private func stopTimer() {
cancellable?.cancel()
cancellable = nil
}
}
43 changes: 26 additions & 17 deletions Sources/Liquid/PrivateViews/LiquidPathView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,53 @@
//

import SwiftUI
import Combine
import Accelerate

struct LiquidPathView: View {

let pointCloud: (x: [Double], y: [Double])
@State var x: AnimatableArray = .zero
@State var y: AnimatableArray = .zero
@State var samples: Int
@State var animate: Bool
let period: TimeInterval
let trigger: Timer.TimerPublisher

var cancellable: Cancellable?

init(path: CGPath, interpolate: Int, samples: Int, period: TimeInterval) {
init(path: CGPath, interpolate: Int, samples: Int, period: TimeInterval, animate: Bool) {
self._samples = .init(initialValue: samples)
self.period = period
self.trigger = Timer.TimerPublisher(interval: period, runLoop: .main, mode: .common)
self.cancellable = self.trigger.connect()
self.pointCloud = path.getPoints().interpolate(interpolate)
self._animate = .init(initialValue: animate)
}

func generate() {
withAnimation(.linear(duration: period)) {
let points = Array(0..<pointCloud.x.count).randomElements(samples)
self.x = AnimatableArray(points.map { self.pointCloud.x[$0] })
self.y = AnimatableArray(points.map { self.pointCloud.y[$0] })
guard animate else { return }

if #available(iOS 17.0, *) {
withAnimation(.linear(duration: period)) {
let points = Array(0..<pointCloud.x.count).randomElements(samples)
self.x = AnimatableArray(points.map { self.pointCloud.x[$0] })
self.y = AnimatableArray(points.map { self.pointCloud.y[$0] })
} completion: {
self.generate()
}
} else {
withAnimation(.linear(duration: period)) {
let points = Array(0..<pointCloud.x.count).randomElements(samples)
self.x = AnimatableArray(points.map { self.pointCloud.x[$0] })
self.y = AnimatableArray(points.map { self.pointCloud.y[$0] })
}
Task.delayed(by: .seconds(period)) { @MainActor in
self.generate()
}
}
}

var body: some View {
LiquidPath(x: x, y: y)
.onReceive(trigger) { _ in
.onAppear {
self.generate()
}.onAppear {
self.generate()
}.onDisappear {
self.cancellable?.cancel()
}
.onDisappear {
animate = false
}
}
}
18 changes: 18 additions & 0 deletions Sources/Liquid/Processing/Extensions/Task+Delay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// File.swift
//
//
// Created by Bence Borsos on 18/08/2024.
//

import Foundation

extension Task where Failure == Error {
/// Create a delayed task that will wait for the given duration before performing its operation.
@discardableResult static func delayed(by delay: OperationQueue.SchedulerTimeType.Stride, priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) -> Task {
Task(priority: priority) {
try await Task<Never, Never>.sleep(nanoseconds: UInt64(delay.timeInterval * .nanosecondsPerSecond))
return try await operation()
}
}
}
13 changes: 13 additions & 0 deletions Sources/Liquid/Processing/Extensions/TimeInterval.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// File.swift
//
//
// Created by Bence Borsos on 18/08/2024.
//

import Foundation

extension TimeInterval {
/// The number of nanoseconds in one second.
static let nanosecondsPerSecond = TimeInterval(NSEC_PER_SEC)
}
9 changes: 4 additions & 5 deletions Sources/Liquid/PublicViews/Liquid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//

import SwiftUI
import Combine

/// A flowing, liquid view
public struct Liquid: View {
Expand All @@ -17,8 +16,8 @@ public struct Liquid: View {
/// - Parameters:
/// - samples: number of points to sample along the circular path
/// - period: length of animation
public init(samples: Int = 8, period: TimeInterval = 6) {
self.content = AnyView(LiquidCircleView(samples: samples, period: period))
public init(samples: Int = 8, period: TimeInterval = 6, animate: Bool = true) {
self.content = AnyView(LiquidCircleView(samples: samples, period: period, animate: animate))
}

/// A blob resembling a custom path
Expand All @@ -27,9 +26,9 @@ public struct Liquid: View {
/// - interpolate: number of points along the path to up-sample
/// - samples: the number of samples to select at each animation
/// - period: length of animation
public init(_ path: CGPath, interpolate: Int, samples: Int, period: TimeInterval = 6) {
public init(_ path: CGPath, interpolate: Int, samples: Int, period: TimeInterval = 6, animate: Bool = true) {
assert(interpolate > samples)
self.content = AnyView(LiquidPathView(path: path, interpolate: interpolate, samples: samples, period: period))
self.content = AnyView(LiquidPathView(path: path, interpolate: interpolate, samples: samples, period: period, animate: animate))
}

public var body: some View {
Expand Down