diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 806c20c0f5..246daa0e53 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1619,6 +1619,7 @@ B99288002CF124AC000D62C4 /* Text+Links.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99287FF2CF124AC000D62C4 /* Text+Links.swift */; }; B99B155D2A71BA5200E26DAC /* StoryContextViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99B155C2A71BA5200E26DAC /* StoryContextViewState.swift */; }; B9A0807A2B07D76A000FDB5B /* HomeTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A080792B07D76A000FDB5B /* HomeTabViewController.swift */; }; + B9A47ACE2D36DA6B0024DD9C /* circular_indeterminate.json in Resources */ = {isa = PBXBuildFile; fileRef = B9A47ACD2D36DA6B0024DD9C /* circular_indeterminate.json */; }; B9A53B912CF507FB0000578B /* LinkAndSyncProgressModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A53B902CF507FB0000578B /* LinkAndSyncProgressModal.swift */; }; B9A53B932CF7928A0000578B /* SheetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A53B922CF7928A0000578B /* SheetPreviewViewController.swift */; }; B9A53B952CF799590000578B /* LinkOrSyncPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A53B942CF799590000578B /* LinkOrSyncPickerSheet.swift */; }; @@ -5373,6 +5374,7 @@ B99287FF2CF124AC000D62C4 /* Text+Links.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+Links.swift"; sourceTree = ""; }; B99B155C2A71BA5200E26DAC /* StoryContextViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextViewState.swift; sourceTree = ""; }; B9A080792B07D76A000FDB5B /* HomeTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabViewController.swift; sourceTree = ""; }; + B9A47ACD2D36DA6B0024DD9C /* circular_indeterminate.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = circular_indeterminate.json; sourceTree = ""; }; B9A53B902CF507FB0000578B /* LinkAndSyncProgressModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAndSyncProgressModal.swift; sourceTree = ""; }; B9A53B922CF7928A0000578B /* SheetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetPreviewViewController.swift; sourceTree = ""; }; B9A53B942CF799590000578B /* LinkOrSyncPickerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkOrSyncPickerSheet.swift; sourceTree = ""; }; @@ -10519,6 +10521,7 @@ 34848D5D25D43ADD00E5034B /* add-money.json */, 88DBDFBA2638FFBC00C2101C /* audio-played-dot.json */, 34848D5A25D43ADD00E5034B /* cash-out.json */, + B9A47ACD2D36DA6B0024DD9C /* circular_indeterminate.json */, 880FB3EA28CA53D200FA1C10 /* determinate_spinner_44.json */, 880FB3EC28CA53D300FA1C10 /* determinate_spinner_56.json */, 88E8BEEF28D53C3700509CE2 /* indeterminate_spinner_20.json */, @@ -14471,6 +14474,7 @@ 45B74A7B2044AAB600CD42F8 /* chord.aifc in Resources */, 45B74A892044AAB600CD42F8 /* circles-quiet.aifc in Resources */, 45B74A832044AAB600CD42F8 /* circles.aifc in Resources */, + B9A47ACE2D36DA6B0024DD9C /* circular_indeterminate.json in Resources */, 4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */, 4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */, 45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */, diff --git a/Signal/Lottie/circular_indeterminate.json b/Signal/Lottie/circular_indeterminate.json new file mode 100644 index 0000000000..bd49775694 --- /dev/null +++ b/Signal/Lottie/circular_indeterminate.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":60,"ip":0,"op":110,"w":52,"h":52,"nm":"Progress indicator - Indeterminate - Circular","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":4,"ty":4,"nm":"1","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-26]},{"t":81,"s":[0]}],"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[48,48],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333340287,0.403921574354,0.960784316063,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[5]},{"i":{"x":[0.679],"y":[0.748]},"o":{"x":[0.43],"y":[0]},"t":40,"s":[0]},{"t":109,"s":[87.471]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[6]},{"i":{"x":[0.614],"y":[0.241]},"o":{"x":[0.16],"y":[0]},"t":40,"s":[75]},{"t":109,"s":[89.606]}],"ix":2},"o":{"a":0,"k":1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":110,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Rotator","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":109,"s":[397.294]},{"t":323,"s":[1444]}],"ix":10},"p":{"a":0,"k":[26,26,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[],"ip":0,"op":111,"st":0,"bm":0}],"markers":[]} diff --git a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkAndSyncProgressModal.swift b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkAndSyncProgressModal.swift index f0c8268ca4..821961bfc7 100644 --- a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkAndSyncProgressModal.swift +++ b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkAndSyncProgressModal.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import Lottie import SwiftUI import SignalUI import SignalServiceKit @@ -11,40 +12,67 @@ import SignalServiceKit class LinkAndSyncProgressViewModel: ObservableObject { - enum Phase { - case preparing - case syncing - } - - @Published private(set) var progress: Float = 0 - @Published private(set) var canBeCancelled: Bool = false - @Published var phase: Phase = .preparing - @Published var linkNSyncTask: Task? @Published var didTapCancel: Bool = false + @Published var taskProgress: Float = 0 + @Published var isIndeterminate = true + @Published var canBeCancelled: Bool = false + @Published var linkNSyncTask: Task? + +#if DEBUG + @Published var progressSourceLabel: String? +#endif var cancelButtonEnabled: Bool { linkNSyncTask != nil && canBeCancelled && !didTapCancel } - var title: String { - switch phase { - case .preparing: - OWSLocalizedString( - "LINK_NEW_DEVICE_SYNC_PROGRESS_TITLE_PREPARING", - comment: "Title for a progress modal indicating the sync progress while it's preparing for upload" - ) - case .syncing: - OWSLocalizedString( - "LINK_NEW_DEVICE_SYNC_PROGRESS_TITLE", - comment: "Title for a progress modal indicating the sync progress" - ) - } + var progress: Float { + didTapCancel ? 0 : taskProgress } - func updateProgress(progress: Float, canBeCancelled: Bool) { - self.progress = progress + fileprivate func updateProgress(progress: Float, canBeCancelled: Bool) { + withAnimation(.smooth) { + self.taskProgress = progress + } self.canBeCancelled = canBeCancelled } + + func updateProgress(progress: OWSProgress) { + // This seems to help with the Lottie bug mentioned below + objectWillChange.send() + + let canBeCancelled: Bool + if let label = progress.currentSourceLabel { + canBeCancelled = label != PrimaryLinkNSyncProgressPhase.waitingForLinking.rawValue + } else { + canBeCancelled = false + } + + if progress.completedUnitCount > PrimaryLinkNSyncProgressPhase.waitingForLinking.percentOfTotalProgress { + self.isIndeterminate = false + } + + updateProgress( + progress: progress.percentComplete, + canBeCancelled: canBeCancelled + ) + +#if DEBUG + progressSourceLabel = progress.currentSourceLabel +#endif + } + + func cancel() { + linkNSyncTask?.cancel() + withAnimation(.smooth(duration: 0.2)) { + taskProgress = 0 + } + didTapCancel = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in + self?.isIndeterminate = true + } + } } // MARK: Hosting Controller @@ -93,6 +121,11 @@ struct LinkAndSyncProgressView: View { @ObservedObject fileprivate var viewModel: LinkAndSyncProgressViewModel + @State private var indeterminateProgressShouldShow = false + private var loopMode: LottieLoopMode { + viewModel.isIndeterminate ? .loop : .playOnce + } + // If the first portion fills very quickly before the view is visible, // we still want to animate it from 0. private var progressToShow: Float { @@ -100,21 +133,77 @@ struct LinkAndSyncProgressView: View { case .appearing: 0 case .cancelled, .finished, .none: - viewModel.progress + indeterminateProgressShouldShow ? 0 : viewModel.progress + } + } + + private var showIndeterminateProgress: Bool { + switch appearanceTransitionState { + case .none, .appearing, .cancelled: + false + case .finished: + viewModel.isIndeterminate || indeterminateProgressShouldShow + } + } + + private var title: String { + if viewModel.didTapCancel { + OWSLocalizedString( + "LINK_NEW_DEVICE_SYNC_PROGRESS_TILE_CANCELLING", + comment: "Title for a progress modal that would be indicating the sync progress while it's cancelling that sync" + ) + } else if indeterminateProgressShouldShow || appearanceTransitionState != .finished { + OWSLocalizedString( + "LINK_NEW_DEVICE_SYNC_PROGRESS_TITLE_PREPARING", + comment: "Title for a progress modal indicating the sync progress while it's preparing for upload" + ) + } else { + OWSLocalizedString( + "LINK_NEW_DEVICE_SYNC_PROGRESS_TITLE", + comment: "Title for a progress modal indicating the sync progress" + ) } } var body: some View { VStack(spacing: 0) { - // TODO: this should become an "indefinite" animation when cancelled - CircleProgressView(progress: progressToShow) - .padding(.top, 14) - .padding(.bottom, 20) - .animation(.linear, value: progressToShow) + ZStack { + CircleProgressView(progress: progressToShow) + .animation(.smooth, value: appearanceTransitionState) + .animation(.smooth, value: indeterminateProgressShouldShow) - Text(viewModel.title) + if showIndeterminateProgress { + LottieView(animation: .named("circular_indeterminate")) + .playing(loopMode: loopMode) + .animationDidFinish { completed in + print("animationDidFinish: \(completed)") + guard completed else { return } + indeterminateProgressShouldShow = false + } + .onAppear { + indeterminateProgressShouldShow = true + } + } + } + .padding(.top, 12) + .padding(.bottom, 20) + .onChange(of: viewModel.isIndeterminate) { isIndeterminate in + guard !isIndeterminate else { return } + // There is a seemingly rng bug where the Lottie + // view doesn't properly respond to the change of + // loopMode, leading to .animationDidFinish never + // being called. The animation is a bit over one + // second, so if it's not done after two seconds, + // force hide it. + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.indeterminateProgressShouldShow = false + } + } + + Text(title) .font(.headline) .padding(.bottom, 8) + .animation(.none, value: title) Text(String( format: OWSLocalizedString( @@ -136,11 +225,17 @@ struct LinkAndSyncProgressView: View { .padding(.bottom, 36) Button(CommonStrings.cancelButton) { - viewModel.linkNSyncTask?.cancel() - viewModel.didTapCancel = true + viewModel.cancel() } .disabled(!viewModel.cancelButtonEnabled) .font(.body.weight(.semibold)) + +#if DEBUG + Text("DEBUG: " + (viewModel.progressSourceLabel ?? "none")) + .padding(.top) + .foregroundStyle(Color.Signal.quaternaryLabel) + .animation(.none, value: viewModel.progressSourceLabel) +#endif } .padding(.horizontal, 26) .padding(.vertical, 28) @@ -163,9 +258,9 @@ struct LinkAndSyncProgressView: View { .rotation(.degrees(-90)) .stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round)) .foregroundStyle(Color.Signal.accent) - .animation(.linear, value: progress) } - .frame(width: 52, height: 52) + .frame(width: 48, height: 48) + .padding(2) } } } @@ -173,30 +268,100 @@ struct LinkAndSyncProgressView: View { // MARK: Previews #if DEBUG + +@MainActor @available(iOS 17, *) -#Preview { - SheetPreviewViewController { - let modal = LinkAndSyncProgressModal() - modal.linkNSyncTask = Task {} +private func setupDemoProgress( + modal: LinkAndSyncProgressModal, + slowLinking: Bool +) async throws { + let progress = OWSProgress.createSink { progress in + modal.viewModel.updateProgress(progress: progress) + } - Task { @MainActor in - modal.viewModel.updateProgress(progress: 0.2, canBeCancelled: false) - let loadingPoints = (0..<20) - .map { _ in Float.random(in: (0.2)...1) } - .sorted() + let waitForLinkingProgress = await progress.addSource( + withLabel: PrimaryLinkNSyncProgressPhase.waitingForLinking.rawValue, + unitCount: PrimaryLinkNSyncProgressPhase.waitingForLinking.percentOfTotalProgress + ) + let exportingBackupProgress = await progress.addSource( + withLabel: PrimaryLinkNSyncProgressPhase.exportingBackup.rawValue, + unitCount: PrimaryLinkNSyncProgressPhase.exportingBackup.percentOfTotalProgress + ) + let uploadingBackupProgress = await progress.addSource( + withLabel: PrimaryLinkNSyncProgressPhase.uploadingBackup.rawValue, + unitCount: PrimaryLinkNSyncProgressPhase.uploadingBackup.percentOfTotalProgress + ) + let markUploadedProgress = await progress.addSource( + withLabel: PrimaryLinkNSyncProgressPhase.finishing.rawValue, + unitCount: PrimaryLinkNSyncProgressPhase.finishing.percentOfTotalProgress + ) - for point in loadingPoints { - try? await Task.sleep(for: .milliseconds(100)) - modal.viewModel.updateProgress(progress: point, canBeCancelled: point >= 0.4) - modal.viewModel.phase = point >= 0.6 ? .syncing : .preparing - } + if slowLinking { + try await Task.sleep(for: .milliseconds(700)) + } else { + try await Task.sleep(for: .milliseconds(100)) + } - try? await Task.sleep(for: .milliseconds(100)) - modal.viewModel.updateProgress(progress: 1, canBeCancelled: true) + waitForLinkingProgress.incrementCompletedUnitCount(by: PrimaryLinkNSyncProgressPhase.waitingForLinking.percentOfTotalProgress) - await modal.completeAndDismiss() + if slowLinking { + try await Task.sleep(for: .milliseconds(700)) + } else { + try await Task.sleep(for: .milliseconds(100)) + } + + func simulateProgress(for source: OWSProgressSource) async throws { + for _ in 0..<(source.totalUnitCount / 2) { + source.incrementCompletedUnitCount(by: 2) + try await Task.sleep(for: .milliseconds(50)) } + source.incrementCompletedUnitCount(by: source.totalUnitCount) + } + + try await simulateProgress(for: exportingBackupProgress) + try await simulateProgress(for: uploadingBackupProgress) + + try await Task.sleep(for: .milliseconds(500)) + + try Task.checkCancellation() + markUploadedProgress.incrementCompletedUnitCount(by: PrimaryLinkNSyncProgressPhase.finishing.percentOfTotalProgress) + + await modal.completeAndDismiss() +} + +@MainActor +@available(iOS 17, *) +func demoTask( + modal: LinkAndSyncProgressModal, + slowLinking: Bool +) -> Task { + Task { + do { + try await setupDemoProgress(modal: modal, slowLinking: slowLinking) + } catch { + try? await Task.detached { + try await Task.sleep(for: slowLinking ? .seconds(3) : .milliseconds(500)) + }.value + modal.dismiss(animated: true) + } + } +} + +@available(iOS 17, *) +#Preview("Slow linking") { + SheetPreviewViewController(animateFirstAppearance: true) { + let modal = LinkAndSyncProgressModal() + modal.linkNSyncTask = demoTask(modal: modal, slowLinking: true) + return modal + } +} + +@available(iOS 17, *) +#Preview("Fast linking") { + SheetPreviewViewController(animateFirstAppearance: true) { + let modal = LinkAndSyncProgressModal() + modal.linkNSyncTask = demoTask(modal: modal, slowLinking: false) return modal } } diff --git a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift index 1a0df0f023..5204c98925 100644 --- a/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift +++ b/Signal/src/ViewControllers/AppSettings/Linked Devices/LinkedDevicesView.swift @@ -282,30 +282,7 @@ extension LinkedDevicesViewModel: LinkDeviceViewControllerDelegate { let progress = OWSProgress.createSink { progress in Task { @MainActor in - let canBeCancelled: Bool - if let label = progress.currentSourceLabel { - canBeCancelled = label != PrimaryLinkNSyncProgressPhase.waitingForLinking.rawValue - } else { - canBeCancelled = false - } - - switch progress.currentSourceLabel { - case - PrimaryLinkNSyncProgressPhase.waitingForLinking.rawValue, - PrimaryLinkNSyncProgressPhase.exportingBackup.rawValue: - linkAndSyncProgressModal.viewModel.phase = .preparing - case - PrimaryLinkNSyncProgressPhase.uploadingBackup.rawValue, - PrimaryLinkNSyncProgressPhase.finishing.rawValue: - linkAndSyncProgressModal.viewModel.phase = .syncing - default: - break - } - - linkAndSyncProgressModal.viewModel.updateProgress( - progress: progress.percentComplete, - canBeCancelled: canBeCancelled - ) + linkAndSyncProgressModal.viewModel.updateProgress(progress: progress) } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 3eb5c6aed6..aa52cb766b 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -4078,6 +4078,9 @@ /* On a progress modal indicating the percent complete the sync process is. Embeds {{ formatted percentage }} */ "LINK_NEW_DEVICE_SYNC_PROGRESS_PERCENT" = "%@ complete"; +/* Title for a progress modal that would be indicating the sync progress while it's cancelling that sync */ +"LINK_NEW_DEVICE_SYNC_PROGRESS_TILE_CANCELLING" = "Cancelling…"; + /* Title for a progress modal indicating the sync progress */ "LINK_NEW_DEVICE_SYNC_PROGRESS_TITLE" = "Syncing Messages…"; diff --git a/SignalServiceKit/Devices/LinkAndSyncManager.swift b/SignalServiceKit/Devices/LinkAndSyncManager.swift index d1f011d860..fb7a36722e 100644 --- a/SignalServiceKit/Devices/LinkAndSyncManager.swift +++ b/SignalServiceKit/Devices/LinkAndSyncManager.swift @@ -57,10 +57,10 @@ public enum PrimaryLinkNSyncProgressPhase: String { case uploadingBackup case finishing - var percentOfTotalProgress: UInt64 { + public var percentOfTotalProgress: UInt64 { return switch self { - case .waitingForLinking: 30 - case .exportingBackup: 25 + case .waitingForLinking: 5 + case .exportingBackup: 50 case .uploadingBackup: 40 case .finishing: 5 } diff --git a/SignalUI/Utils/SheetPreviewViewController.swift b/SignalUI/Utils/SheetPreviewViewController.swift index 45b522145b..c2d79e7bfa 100644 --- a/SignalUI/Utils/SheetPreviewViewController.swift +++ b/SignalUI/Utils/SheetPreviewViewController.swift @@ -7,6 +7,7 @@ import UIKit #if DEBUG public class SheetPreviewViewController: UIViewController { + private let animateFirstAppearance: Bool private let presentAction: PresentAction private enum PresentAction { @@ -25,21 +26,31 @@ public class SheetPreviewViewController: UIViewController { } public init( + animateFirstAppearance: Bool = false, presentSheet: @escaping ( _ viewController: SheetPreviewViewController, _ animated: Bool ) -> Void ) { + self.animateFirstAppearance = animateFirstAppearance self.presentAction = .presentSheet(presentSheet) super.init(nibName: nil, bundle: nil) } - public init(sheet: @escaping @autoclosure () -> UIViewController) { + public init( + animateFirstAppearance: Bool = false, + sheet: @escaping @autoclosure () -> UIViewController + ) { + self.animateFirstAppearance = animateFirstAppearance self.presentAction = .createSheet(sheet) super.init(nibName: nil, bundle: nil) } - public init(createSheet: @escaping () -> UIViewController) { + public init( + animateFirstAppearance: Bool = false, + createSheet: @escaping () -> UIViewController + ) { + self.animateFirstAppearance = animateFirstAppearance self.presentAction = .createSheet(createSheet) super.init(nibName: nil, bundle: nil) } @@ -60,7 +71,7 @@ public class SheetPreviewViewController: UIViewController { public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.presentAction.present(from: self, animated: false) + self.presentAction.present(from: self, animated: animateFirstAppearance) } } #endif