// // PulsingCircleTestApp.swift // PulsingCircleTest // // Created by Steve on 7/19/24. // import SwiftUI import Combine @main struct PulsingCircleTestApp: App { var body: some Scene { WindowGroup { NavigationStack() {//Commenting out will get rid of the unwanted vertical tween ContentView() } } } } // MARK: - Content View struct ContentView: View { @StateObject private var viewModel = MuseViewModel() var body: some View { ZStack { Color.gray.opacity(0.2).edgesIgnoringSafeArea(.all) VStack { Text("Circle Demo") .font(.largeTitle) Spacer() MuseConnectionView(viewModel: viewModel) .frame(height: 60) .background(Color.black.opacity(0.1)) Spacer() Button("Toggle Picker") { viewModel.isScanning.toggle() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) } } } } // MARK: - MuseConnectionView struct MuseConnectionView: View { @ObservedObject var viewModel: MuseViewModel // @Binding var isPickerPresented: Bool var body: some View { HStack(spacing: 20) { PulsingCircle1(isScanning: $viewModel.isScanning) PulsingCircle2(isScanning: $viewModel.isScanning) PulsingCircle3(isScanning: $viewModel.isScanning) PulsingCircle4(isScanning: $viewModel.isScanning) } .onAppear { viewModel.startScanning() } .onDisappear { viewModel.stopScanning() } } } // MARK: - ViewModel class MuseViewModel: ObservableObject { @Published var isScanning = false func startScanning() { guard !isScanning else { return } isScanning = true } func stopScanning() { isScanning = false } } // MARK: - PulsingCircle Views struct PulsingCircle1: View { @Binding var isScanning: Bool @State private var scale: CGFloat = 1.0 var body: some View { ZStack { Circle() .fill(Color.blue.opacity(0.3)) .scaleEffect(scale) .opacity(isScanning ? 0.7 : 0) Circle() .fill(Color.white) .overlay( Image(systemName: "1.circle") .foregroundColor(.gray) ) } .frame(width: 44, height: 44) .animation(.easeInOut(duration: 1).repeatForever(autoreverses: true), value: isScanning) .onChange(of: isScanning) { newValue in scale = newValue ? 1.2 : 1.0 } } } // MARK: - Preview struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct PulsingCircle2: View { @Binding var isScanning: Bool @State private var scale: CGFloat = 1.0 var body: some View { ZStack { Circle() .fill(Color.blue.opacity(0.3)) .scaleEffect(scale) .opacity(isScanning ? 0.7 : 0) Circle() .fill(Color.white) .overlay( Image(systemName: "2.circle") .foregroundColor(.gray) ) } .frame(width: 44, height: 44) .onChange(of: isScanning) { newValue in if newValue { withAnimation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)) { scale = 1.2 } } else { scale = 1.0 } } } } struct PulsingCircle3: View { @Binding var isScanning: Bool @State private var isAnimating = false var body: some View { ZStack { Circle() .fill(Color.blue.opacity(0.3)) .scaleEffect(isAnimating ? 1.2 : 1.0) .opacity(isScanning ? 0.7 : 0) Circle() .fill(Color.white) .overlay( Image(systemName: "3.circle") .foregroundColor(.gray) ) } .frame(width: 44, height: 44) .onChange(of: isScanning) { newValue in if newValue { withAnimation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)) { isAnimating = true } } else { isAnimating = false } } } } struct PulsingCircle4: View { @Binding var isScanning: Bool @State private var scale: CGFloat = 1.0 var body: some View { ZStack { Circle() .fill(Color.blue.opacity(0.3)) .scaleEffect(scale) .opacity(isScanning ? 0.7 : 0) Circle() .fill(Color.white) .overlay( Image(systemName: "4.circle") .foregroundColor(.gray) ) } .frame(width: 44, height: 44) .onChange(of: isScanning) { newValue in if newValue { withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) { scale = 1.2 } } else { withAnimation(.easeInOut(duration: 0.3)) { scale = 1.0 } } } } }