Reputation: 395
I am trying to make similar Speaker Animation in SwiftUI as that of Apple's Control Center Speaker or Brightness HUD. I'm using 4 images from SFSymbols provided by Apple:
// speakerEmpty
// speaker1
// speaker2
// speaker3
So, I have a Switch (Toggle in SwiftUI) to toggle my Sound option with an animation of four images. This is how I want images to be animated: When the toggle is on:
speakerEmpty
should animate to -> speaker1
thenspeaker1
should animate to -> speaker2
thenspeaker2
should animate to -> speaker3
And reverse for when the toggle is off.
Here is the code I tried:
SpeakerSymbol Enum
import SwiftUI
enum SpeakerSymbol:CaseIterable {
case speakerEmpty, speaker1, speaker2, speaker3
var image:Image {
switch self {
case .speakerEmpty: return Image(systemName: "speaker.slash.fill")
case .speaker1: return Image(systemName: "speaker.1.fill")
case .speaker2: return Image(systemName: "speaker.2.fill")
case .speaker3: return Image(systemName: "speaker.3.fill")
}
}
}
Speaker Selection Observable Object
final class SpeakerSelection: ObservableObject {
@Published var selectedSymbol:SpeakerSymbol = .speaker3
@Published var isSoundEnabled = true {
didSet {
if isSoundEnabled {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.selectedSymbol = .speakerEmpty
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.selectedSymbol = .speaker1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.selectedSymbol = .speaker2
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.selectedSymbol = .speaker3
}
} else {
selectedSymbol = .speaker3
selectedSymbol = .speaker2
selectedSymbol = .speaker1
selectedSymbol = .speakerEmpty
}
}
}
}
Profile View
struct ProfileView: View {
@ObservedObject var selection = SpeakerSelection()
var body: some View {
NavigationView {
VStack {
Spacer()
Text("Speaker Toggle").font(.largeTitle)
Spacer()
Toggle(isOn: withAnimation {
$selection.isSoundEnabled
}, label: {
selection.selectedSymbol.image
.frame(width: 50)
.animation(Animation.default.delay(0.2))
.transition(.asymmetric(insertion: .opacity, removal: .opacity))
// speakerSymbol()
Text("Sound")
}).padding().animation(.default).font(.largeTitle)
Spacer()
Spacer()
}
}
}
func speakerSymbol() -> AnyView {
switch selection.selectedSymbol {
case .speakerEmpty: return AnyView(SpeakerSymbol.speakerEmpty.image
.animation(Animation.default.delay(0.2))
.transition(.asymmetric(insertion: .opacity, removal: .opacity)))
case .speaker1: return AnyView(SpeakerSymbol.speaker1.image
.animation(Animation.default.delay(0.2))
.transition(.asymmetric(insertion: .opacity, removal: .opacity)))
case .speaker2: return AnyView(SpeakerSymbol.speaker2.image
.animation(Animation.default.delay(0.2))
.transition(.asymmetric(insertion: .opacity, removal: .opacity)))
case .speaker3: return AnyView(SpeakerSymbol.speaker3.image
.animation(Animation.default.delay(0.2))
.transition(.asymmetric(insertion: .opacity, removal: .opacity)))
}
}
}
I guess my code logic is fine. Images are replacing fine as it should. But there is no animation effect working. It's frustrating I've tried a bunch of possible ways but none is working.
Upvotes: 2
Views: 1249
Reputation: 257613
Here is possible approach. I simplified it for a demo purpose and less posting code, but idea should be clear and easy transferrable to your real code
Result demo (really it is much fluent than on gif):
Modified model
enum SpeakerSymbol: Int, CaseIterable { // Inherited from Int for convenient below
case speakerEmpty, speaker1, speaker2, speaker3
var image: some View {
var name: String
switch self {
case .speakerEmpty: name = "speaker.slash.fill"
case .speaker1: name = "speaker.1.fill"
case .speaker2: name = "speaker.2.fill"
case .speaker3: name = "speaker.3.fill"
}
return Image(systemName: name).font(.largeTitle)
}
}
Animatable modifier for SpeakerSymbol, required to let SwiftUI animation know that changed SpeakerSymbol value is able to animate
struct SpeakerModifier: AnimatableModifier {
var symbol: SpeakerSymbol
init(symbol: SpeakerSymbol) {
self.symbol = symbol
self.animating = Double(symbol.rawValue) // enum to Double
}
private var animating: Double // Double supports Animatable
var animatableData: Double { // required part of Animatable protocol
get { animating }
set { animating = newValue }
}
func body(content: Content) -> some View {
return SpeakerSymbol(rawValue: Int(animating))!.image // Double -> enum
}
}
Demo of usage
struct TestSpeakerModifier: View {
@State private var speaker: SpeakerSymbol = .speakerEmpty
var body: some View {
VStack {
Color.clear // << just holder area
.modifier(SpeakerModifier(symbol: speaker))
.frame(width: 60, height: 60, alignment: .leading)
Divider()
Button("Toggle") {
withAnimation { // animates between enum states
self.speaker =
(self.speaker == .speakerEmpty ? .speaker3 : .speakerEmpty)
}
}
}
}
}
Upvotes: 2