Skip to content

Commit 2f611f6

Browse files
authored
Merge pull request #363 from Esri/Caleb/Fix-Animate3DGraphicAnimation
[Fix] `Animate 3D graphic` animations
2 parents 292bbd5 + 02c75a8 commit 2f611f6

File tree

2 files changed

+58
-56
lines changed

2 files changed

+58
-56
lines changed

Shared/Samples/Animate 3D graphic/Animate3DGraphicView.Model.swift

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,15 @@ extension Animate3DGraphicView {
128128
@Published private(set) var cameraPropertyTexts: [CameraProperty: String] = [:]
129129

130130
init() {
131-
// Set up the mission and the graphics.
131+
// Set up the mission, graphics, and animation.
132132
updateMission()
133+
134+
let displayLink = CADisplayLink(target: self, selector: #selector(updatePositions))
135+
animation.setup(displayLink: displayLink)
136+
}
137+
138+
deinit {
139+
Task { await animation.displayLink?.invalidate() }
133140
}
134141

135142
// MARK: Methods
@@ -155,24 +162,6 @@ extension Animate3DGraphicView {
155162
}
156163
}
157164

158-
/// Starts a new animation by creating a timer used to move the graphics.
159-
func startAnimation() {
160-
// Stop any previous on going animation.
161-
animation.stop()
162-
animation.isPlaying = true
163-
164-
// Create a new timer to update the graphics' position each iteration.
165-
let interval = 1 / Double(animation.speed)
166-
animation.timer = Timer.scheduledTimer(
167-
timeInterval: interval,
168-
target: self,
169-
selector: #selector(updatePositions),
170-
userInfo: nil,
171-
repeats: true
172-
)
173-
RunLoop.current.add(animation.timer!, forMode: .common)
174-
}
175-
176165
/// Updates the text associated with a given camera controller property.
177166
/// - Parameters:
178167
/// - property: The camera controller property associated with the text to update.
@@ -223,14 +212,18 @@ extension Animate3DGraphicView {
223212

224213
/// A struct containing data for an animation.
225214
struct Animation {
226-
/// The timer for the animation used to loop through the animation frames.
227-
var timer: Timer?
215+
/// The timer used to loop through the animation frames.
216+
private(set) var displayLink: CADisplayLink?
228217

229-
/// The speed of the animation used to set the timer's time interval.
230-
var speed = 50.0
218+
/// The speed of the animation.
219+
var speed = AnimationSpeed.medium
231220

232-
/// A Boolean that indicates whether the animation is currently playing.
233-
var isPlaying = false
221+
/// A Boolean value indicating whether the animation is currently playing.
222+
var isPlaying = false {
223+
didSet {
224+
displayLink?.isPaused = !isPlaying
225+
}
226+
}
234227

235228
/// The current frame of the animation.
236229
var currentFrame: Frame {
@@ -255,26 +248,31 @@ extension Animate3DGraphicView {
255248
/// The index of the current frame in the frames list.
256249
private var currentFrameIndex = 0
257250

258-
/// Stops the animation by invalidating the timer.
259-
mutating func stop() {
260-
timer?.invalidate()
261-
isPlaying = false
251+
/// Sets up the animation using a given display link.
252+
/// - Parameter displayLink: The display link used to run the animation.
253+
mutating func setup(displayLink: CADisplayLink) {
254+
// Add the display link to main thread common mode run loop,
255+
// so it is not effected by UI events.
256+
displayLink.add(to: .main, forMode: .common)
257+
displayLink.preferredFramesPerSecond = 60
258+
self.displayLink = displayLink
262259
}
263260

264261
/// Resets the animation to the beginning.
265262
mutating func reset() {
266-
stop()
263+
isPlaying = false
267264
currentFrameIndex = 0
268265
}
269266

270-
/// Increments the animation to the next frame.
267+
/// Increments the animation to the next frame based on the speed.
271268
mutating func nextFrame() {
272-
if currentFrameIndex >= framesCount - 1 {
269+
// Increment the frame index using the current speed.
270+
let nextFrameIndex = currentFrameIndex + speed.rawValue
271+
if frames.indices.contains(nextFrameIndex) {
272+
currentFrameIndex = nextFrameIndex
273+
} else {
273274
// Reset the animation when it has reached the end.
274275
reset()
275-
} else {
276-
// Move the index to point to the next frame.
277-
currentFrameIndex += 1
278276
}
279277
}
280278

@@ -359,6 +357,13 @@ extension Animate3DGraphicView {
359357
}
360358
}
361359
}
360+
361+
/// An enumeration representing the speed of the animation.
362+
enum AnimationSpeed: Int, CaseIterable {
363+
case slow = 1
364+
case medium = 2
365+
case fast = 4
366+
}
362367
}
363368

364369
private extension FormatStyle where Self == FloatingPointFormatStyle<Double> {

Shared/Samples/Animate 3D graphic/Animate3DGraphicView.swift

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ struct Animate3DGraphicView: View {
4242
StatRow("Roll", value: model.animation.currentFrame.roll.formatted(.angle))
4343
}
4444
.frame(width: 170, height: 100)
45+
.padding([.leading, .trailing])
4546
.background(.ultraThinMaterial)
4647
.cornerRadius(10)
4748
.shadow(radius: 3)
@@ -59,7 +60,9 @@ struct Animate3DGraphicView: View {
5960
.attributionBarHidden(true)
6061
.onSingleTapGesture { _, _ in
6162
// Show/hide full map on tap.
62-
isShowingFullMap.toggle()
63+
withAnimation(.default.speed(2)) {
64+
isShowingFullMap.toggle()
65+
}
6366
}
6467
.frame(width: isShowingFullMap ? nil : 100, height: isShowingFullMap ? nil : 100)
6568
.cornerRadius(10)
@@ -79,7 +82,7 @@ struct Animate3DGraphicView: View {
7982

8083
/// The play/pause button for the animation.
8184
Button {
82-
model.animation.isPlaying ? model.animation.stop() : model.startAnimation()
85+
model.animation.isPlaying.toggle()
8386
} label: {
8487
Image(systemName: model.animation.isPlaying ? "pause.fill" : "play.fill")
8588
}
@@ -93,15 +96,18 @@ struct Animate3DGraphicView: View {
9396
.task {
9497
await model.monitorCameraController()
9598
}
96-
.onDisappear {
97-
model.animation.stop()
98-
}
9999
}
100100

101101
/// The list containing the mission settings.
102102
private var missionSettings: some View {
103103
List {
104104
Section("Mission") {
105+
VStack {
106+
StatRow("Progress", value: model.animation.progress.formatted(.rounded))
107+
ProgressView(value: model.animation.progress)
108+
}
109+
.padding(.vertical)
110+
105111
Picker("Mission Selection", selection: $model.currentMission) {
106112
ForEach(Mission.allCases, id: \.self) { mission in
107113
Text(mission.label)
@@ -111,22 +117,14 @@ struct Animate3DGraphicView: View {
111117
.labelsHidden()
112118
}
113119

114-
Section {
115-
VStack {
116-
StatRow("Animation Speed", value: model.animation.speed.formatted())
117-
Slider(value: $model.animation.speed, in: 1...200, step: 1)
118-
.onChange(of: model.animation.speed) { _ in
119-
if model.animation.isPlaying {
120-
model.startAnimation()
121-
}
122-
}
123-
.padding(.horizontal)
124-
}
125-
VStack {
126-
StatRow("Mission Progress", value: model.animation.progress.formatted(.rounded))
127-
ProgressView(value: model.animation.progress)
128-
.padding()
120+
Section("Speed") {
121+
Picker("Animation Speed", selection: $model.animation.speed) {
122+
ForEach(AnimationSpeed.allCases, id: \.self) { speed in
123+
Text(String(describing: speed).capitalized)
124+
}
129125
}
126+
.pickerStyle(.inline)
127+
.labelsHidden()
130128
}
131129
}
132130
}
@@ -183,7 +181,6 @@ extension Animate3DGraphicView {
183181
Spacer()
184182
Text(value)
185183
}
186-
.padding([.leading, .trailing])
187184
}
188185
}
189186
}

0 commit comments

Comments
 (0)