Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions WordPress/Classes/Extensions/ProgressHUDModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import SVProgressHUD
import SwiftUI

enum ProgressHUDState: Equatable {
case idle
case running
case success
case failure(String)
}

private struct ProgressHUDModifier: ViewModifier {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the production version, instead of a modal HUD, the app shows activity indicators on a per-cell level. It doesn't block your flow and allow you to perform multiple operations in parallel and not wait for completion.

@Binding var state: ProgressHUDState
@State private var dismissTask: Task<Void, Never>?

func body(content: Content) -> some View {
content
.onChange(of: state) { _, newValue in
dismissTask?.cancel()
dismissTask = nil

switch newValue {
case .idle:
break
case .running:
SVProgressHUD.show()
case .success:
SVProgressHUD.showSuccess(withStatus: nil)
dismissAndReset()
case .failure(let message):
SVProgressHUD.showError(withStatus: message)
dismissAndReset()
}
}
}

private func dismissAndReset() {
dismissTask = Task {
try? await Task.sleep(for: .seconds(1))
guard !Task.isCancelled else { return }
await SVProgressHUD.dismiss()
state = .idle
}
}
}

extension View {
func progressHUD(state: Binding<ProgressHUDState>) -> some View {
modifier(ProgressHUDModifier(state: state))
}
}

// MARK: - Preview

#Preview("ProgressHUD Race Condition") {
@Previewable @State var state: ProgressHUDState = .idle

VStack(spacing: 20) {
// Expected: .idle → .running → .success → .idle
Button("Run & Succeed") {
state = .running
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
state = .success
}
}

// Expected: .idle → .running → .failure → .idle
Button("Run & Fail") {
state = .running
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
state = .failure("Something went wrong")
}
}

// Expected: .idle → .success → .running (spinner stays on screen)
Button("Quick Succession (auto)") {
state = .success
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
state = .running
}
}

Text("State: \(String(describing: state))")
.font(.headline)
.animation(.none, value: state)
}
.progressHUD(state: $state)
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ import WordPressData
blog: Blog,
postID: NSNumber? = nil,
delegate: BlazeWebViewControllerDelegate? = nil) {
let navigationViewController = makeBlazeWebViewController(source: source, blog: blog, postID: postID, delegate: delegate)
viewController.present(navigationViewController, animated: true)
}

/// Creates and returns a configured Blaze web view controller wrapped in a navigation controller,
/// without presenting it.
static func makeBlazeWebViewController(
source: BlazeSource,
blog: Blog,
postID: NSNumber? = nil,
delegate: BlazeWebViewControllerDelegate? = nil
) -> UINavigationController {
let blazeViewController = BlazeWebViewController(delegate: delegate)
let viewModel = BlazeCreateCampaignWebViewModel(source: source,
blog: blog,
Expand All @@ -92,7 +104,7 @@ import WordPressData
let navigationViewController = UINavigationController(rootViewController: blazeViewController)
navigationViewController.overrideUserInterfaceStyle = .light
navigationViewController.modalPresentationStyle = .formSheet
viewController.present(navigationViewController, animated: true)
return navigationViewController
}

/// Used to display the blaze overlay.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,12 @@ extension BlogDetailsViewController {
let rootView = ApplicationPasswordRequiredView(
blog: blog,
localizedFeatureName: feature,
presentingViewController: self) { [blog] client in
CustomPostTypesView(blog: blog, service: CustomPostTypeService(client: client, blog: blog))
presentingViewController: self) { [blog, weak self] client in
CustomPostTypesView(
blog: blog,
service: CustomPostTypeService(client: client, blog: blog),
presentingViewController: self
)
}
let controller = UIHostingController(rootView: rootView)
controller.navigationItem.largeTitleDisplayMode = .never
Expand All @@ -114,8 +118,13 @@ extension BlogDetailsViewController {
let rootView = ApplicationPasswordRequiredView(
blog: blog,
localizedFeatureName: feature,
presentingViewController: self) { [blog] client in
PinnedPostTypeView(blog: blog, service: CustomPostTypeService(client: client, blog: blog), postType: postType)
presentingViewController: self) { [blog, weak self] client in
PinnedPostTypeView(
blog: blog,
service: CustomPostTypeService(client: client, blog: blog),
postType: postType,
presentingViewController: self
)
}
let controller = UIHostingController(rootView: rootView)
controller.navigationItem.largeTitleDisplayMode = .never
Expand Down
Loading