From a35fe7d6a35fd0459d614764bd440aa374b2fc8a Mon Sep 17 00:00:00 2001 From: Jan Stehlik Date: Tue, 1 Apr 2025 10:35:38 +0200 Subject: [PATCH 1/3] Extracted some logic from body into separate functions, for cleaner UI. Replaced GeometryReader with better onScrollGeometryChange. --- DynamicParticles/Helper.swift | 6 +- DynamicParticles/ParticleAnimationView.swift | 91 +++++++++----------- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/DynamicParticles/Helper.swift b/DynamicParticles/Helper.swift index 62fa7cf..ae4254b 100644 --- a/DynamicParticles/Helper.swift +++ b/DynamicParticles/Helper.swift @@ -8,11 +8,15 @@ import SwiftUI -enum ParticleState : String, CaseIterable { +enum ParticleState : String, CaseIterable, Identifiable { case idle case listening case speaking case question + + var id: String { + self.rawValue + } } diff --git a/DynamicParticles/ParticleAnimationView.swift b/DynamicParticles/ParticleAnimationView.swift index 77dcb75..dcd6a43 100644 --- a/DynamicParticles/ParticleAnimationView.swift +++ b/DynamicParticles/ParticleAnimationView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import UIKit struct ParticleAnimation: View { @@ -24,66 +23,64 @@ struct ParticleAnimation: View { var body: some View { Canvas { context, size in - context.blendMode = .normal - let mutedColors: [Color] = [ - Color(red: 0.2, green: 0.7, blue: 0.6), - Color(red: 1.0, green: 0.8, blue: 0.6), - Color(red: 0.6, green: 1.0, blue: 0.8), - Color(red: 0.8, green: 0.6, blue: 0.7), - Color(red: 0.6, green: 0.8, blue: 0.7) - ] - - for (index, particle) in particles.enumerated() { - let path = Path(ellipseIn: CGRect(x: particle.x, y: particle.y, width: 3, height: 3)) - let color = mutedColors[index % mutedColors.count].opacity(1.0) - context.fill(path, with: .color(color)) - } + renderCanvas(&context) } .onReceive(timer) { _ in updateParticles() } - .onChange(of: text) { - createParticles() - } - .onAppear { + .onChange(of: text, initial: true) { createParticles() } - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { value in - dragPosition = value.location - dragVelocity = value.velocity - triggerHapticFeedback() - } - - .onEnded { value in - dragPosition = nil - dragVelocity = nil - updateParticles() - } - ) + .gesture(gesture) .background(.background) - .overlay( - GeometryReader { geometry in - Color.clear - .onAppear { - size = geometry.size - text = "circle.fill" - createParticles() - } - } - ) + .onGeometryChange(for: CGSize.self) { geometry in + geometry.size + } action: { newValue in + size = newValue + text = "circle.fill" + } Picker("State", selection: $state) { - ForEach(ParticleState.allCases, id: \.self) { state in + ForEach(ParticleState.allCases) { state in Text(state.rawValue).tag(state) } } - .pickerStyle(SegmentedPickerStyle()) + .pickerStyle(.segmented) .padding() } - + + private func renderCanvas(_ context: inout GraphicsContext) { + context.blendMode = .normal + let mutedColors: [Color] = [ + Color(red: 0.2, green: 0.7, blue: 0.6), + Color(red: 1.0, green: 0.8, blue: 0.6), + Color(red: 0.6, green: 1.0, blue: 0.8), + Color(red: 0.8, green: 0.6, blue: 0.7), + Color(red: 0.6, green: 0.8, blue: 0.7) + ] + + for (index, particle) in particles.enumerated() { + let path = Path(ellipseIn: CGRect(x: particle.x, y: particle.y, width: 3, height: 3)) + let color = mutedColors[index % mutedColors.count].opacity(1.0) + context.fill(path, with: .color(color)) + } + } + + private var gesture: some Gesture { + DragGesture(minimumDistance: 0) + .onChanged { value in + dragPosition = value.location + dragVelocity = value.velocity + triggerHapticFeedback() + } + .onEnded { value in + dragPosition = nil + dragVelocity = nil + updateParticles() + } + } + private func createParticles() { let renderer = ImageRenderer(content: Image(systemName @@ -160,8 +157,6 @@ struct ParticleAnimation: View { } } - - func triggerHapticFeedback() { let impact = UIImpactFeedbackGenerator(style: .light) impact.impactOccurred() From 3d896c988321c8b5261437120de61987a925266c Mon Sep 17 00:00:00 2001 From: Jan Stehlik Date: Tue, 1 Apr 2025 11:01:54 +0200 Subject: [PATCH 2/3] Simplified code to update particles --- DynamicParticles/Helper.swift | 19 ++++++++- DynamicParticles/ParticleAnimationView.swift | 44 ++++---------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/DynamicParticles/Helper.swift b/DynamicParticles/Helper.swift index ae4254b..cccdb42 100644 --- a/DynamicParticles/Helper.swift +++ b/DynamicParticles/Helper.swift @@ -17,6 +17,24 @@ enum ParticleState : String, CaseIterable, Identifiable { var id: String { self.rawValue } + + var text: String { + switch self { + case .idle, .listening, .speaking: + return "circle.fill" + case .question: + return "questionmark" + } + } + + var animation: Animation { + switch self { + case .listening: + .spring() + default: + .linear(duration: 0.5) + } + } } @@ -47,7 +65,6 @@ struct Particle { circulate() } - if let dragPosition = dragPosition { applyDragEffect(dragPosition: dragPosition, dragVelocity: dragVelocity) } diff --git a/DynamicParticles/ParticleAnimationView.swift b/DynamicParticles/ParticleAnimationView.swift index dcd6a43..c16db1f 100644 --- a/DynamicParticles/ParticleAnimationView.swift +++ b/DynamicParticles/ParticleAnimationView.swift @@ -16,8 +16,7 @@ struct ParticleAnimation: View { @State private var state: ParticleState = .idle @State private var dragPosition: CGPoint? @State private var dragVelocity: CGSize? - @State private var text: String = "circle.fill" - + let timer = Timer.publish(every: 1/120, on: .main, in: .common).autoconnect() var body: some View { @@ -28,7 +27,7 @@ struct ParticleAnimation: View { .onReceive(timer) { _ in updateParticles() } - .onChange(of: text, initial: true) { + .onChange(of: state, initial: true) { createParticles() } .gesture(gesture) @@ -37,7 +36,6 @@ struct ParticleAnimation: View { geometry.size } action: { newValue in size = newValue - text = "circle.fill" } Picker("State", selection: $state) { @@ -84,7 +82,7 @@ struct ParticleAnimation: View { private func createParticles() { let renderer = ImageRenderer(content: Image(systemName - : text) + : state.text) .resizable() .scaledToFit() .frame(width: 360, height: 360) @@ -121,37 +119,11 @@ struct ParticleAnimation: View { } private func updateParticles() { - - withAnimation(.linear(duration: 0.5)) { - switch state { - case .idle: - for i in particles.indices { - text = "circle.fill" - particles[i].update(state: .idle, dragPosition: dragPosition, dragVelocity: dragVelocity) - } - case .listening: - for i in particles.indices { - withAnimation(.spring()){ - text = "circle.fill" - - particles[i].update(state: .listening, dragPosition: dragPosition, dragVelocity: dragVelocity) - - } - - } - case .speaking: - for i in particles.indices { - text = "circle.fill" - - particles[i].update(state: .speaking, dragPosition: dragPosition, dragVelocity: dragVelocity) - } - case .question: - for i in particles.indices { - text = "questionmark" - - particles[i].update(state: .idle, dragPosition: dragPosition, dragVelocity: dragVelocity) - } - + withAnimation(state.animation) { + for i in particles.indices { + particles[i].update(state: state, + dragPosition: dragPosition, + dragVelocity: dragVelocity) } } } From cdd7160697e3f3896db98a752c665690f7d7e443 Mon Sep 17 00:00:00 2001 From: Jan Stehlik Date: Tue, 1 Apr 2025 11:20:17 +0200 Subject: [PATCH 3/3] Fixed Timer interfering with Picker --- DynamicParticles/ParticleAnimationView.swift | 9 ++----- DynamicParticles/PickerView.swift | 27 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 DynamicParticles/PickerView.swift diff --git a/DynamicParticles/ParticleAnimationView.swift b/DynamicParticles/ParticleAnimationView.swift index c16db1f..52b59d5 100644 --- a/DynamicParticles/ParticleAnimationView.swift +++ b/DynamicParticles/ParticleAnimationView.swift @@ -38,13 +38,8 @@ struct ParticleAnimation: View { size = newValue } - Picker("State", selection: $state) { - ForEach(ParticleState.allCases) { state in - Text(state.rawValue).tag(state) - } - } - .pickerStyle(.segmented) - .padding() + PickerView(selectedState: $state) + .padding() } diff --git a/DynamicParticles/PickerView.swift b/DynamicParticles/PickerView.swift new file mode 100644 index 0000000..2c98ec6 --- /dev/null +++ b/DynamicParticles/PickerView.swift @@ -0,0 +1,27 @@ +// +// PickerView.swift +// DynamicParticles +// +// Created by Jan StehlĂ­k on 01.04.2025. +// +import SwiftUI + +// Extracted to avoid SwiftUI bug where Timer interferes with Picker. +struct PickerView: View { + @Binding var selectedState: ParticleState + + var body: some View { + Picker("State", selection: $selectedState) { + ForEach(ParticleState.allCases) { state in + Text(state.rawValue).tag(state) + } + } + .pickerStyle(.segmented) + .padding() + } +} + +#Preview { + @Previewable @State var selectedState: ParticleState = .idle + PickerView(selectedState: $selectedState) +}