Created
June 15, 2023 14:45
-
-
Save PhilipTrauner/a7ba179495c9b9946a9d8090e8a3a948 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
private struct Waiter { | |
let timer: Timer? | |
let waiter: Task<Void, Never> | |
func cancel() { | |
timer?.invalidate() | |
waiter.cancel() | |
} | |
} | |
private struct ModalComponent: View { | |
let presentAbort: Bool | |
let abort: () -> Void | |
public init(presentAbort: Bool, abort: @escaping () -> Void) { | |
self.presentAbort = presentAbort | |
self.abort = abort | |
} | |
public var body: some View { | |
ZStack(alignment: .center) { | |
Color(uiColor: .systemBackground) | |
.opacity(0.8) | |
.edgesIgnoringSafeArea(.all) | |
VStack(spacing: 24) { | |
ProgressView() | |
.progressViewStyle(.circular) | |
if presentAbort { | |
Button("abort") { | |
abort() | |
} | |
.transition(.opacity) | |
} | |
} | |
.tint(Color(uiColor: .label)) | |
} | |
.animation(.easeInOut, value: presentAbort) | |
} | |
} | |
struct ModalViewModifier: ViewModifier { | |
let title: Text | |
@Binding var task: Task<Void, Error>? | |
let canAbort: Bool | |
@State private var waiter: Waiter? = .none | |
@State private var error: Error? | |
@State private var presentAbort: Bool = false | |
private func manageTask(_ current: Task<Void, Error>?) { | |
waiter?.cancel() | |
presentAbort = false | |
if let current { | |
var timer: Timer? = .none | |
if canAbort { | |
let new: Timer = .scheduledTimer(withTimeInterval: 2, repeats: false) { _ in | |
presentAbort = true | |
} | |
timer = new | |
RunLoop.main.add(new, forMode: .default) | |
} | |
waiter = .init( | |
timer: timer, | |
waiter: Task { | |
let result = await current.result | |
if case let .failure(inner) = result, | |
case .none = inner as? CancellationError | |
{ | |
error = inner | |
} | |
/// refers to cancellation of waiter, not that of managed task | |
/// cancellation occurs whenever a new managed task is assigned | |
/// condition ensures that the waiter for the previous managed task doesn't zero out the current managed task | |
if !Task.isCancelled { | |
task = .none | |
} | |
} | |
) | |
} | |
} | |
var pending: Bool { | |
if let task, !task.isCancelled { | |
return true | |
} | |
return false | |
} | |
func body(content: Content) -> some View { | |
let presentingAlert: Binding<Bool> = .init( | |
get: { error != nil }, | |
set: { | |
if !$0 { | |
error = .none | |
} | |
} | |
) | |
content | |
.overlay { | |
if pending { | |
ModalComponent( | |
presentAbort: presentAbort, | |
abort: { task?.cancel() } | |
) | |
} | |
} | |
.onAppear { | |
manageTask(task) | |
} | |
.onChange(of: task) { old, new in | |
manageTask(new) | |
} | |
.onDisappear { | |
task?.cancel() | |
} | |
.animation(.easeInOut, value: pending) | |
.alert(title, isPresented: presentingAlert) { | |
Button("OK", role: .cancel) { | |
task = .none | |
} | |
} message: { | |
let message = error?.localizedDescription ?? "unknown error" | |
Text(message) | |
} | |
} | |
} | |
public extension View { | |
@ViewBuilder func modal( | |
title: Text, | |
task: Binding<Task<Void, Error>?>, | |
canAbort: Bool = true | |
) -> some View { | |
modifier(ModalViewModifier(title: title, task: task, canAbort: canAbort)) | |
} | |
} | |
struct ExampleView: View { | |
@State private var task: Optional<Task<(), Error>> = .none | |
var body: some View { | |
Button { | |
task = Task { | |
try await Task.sleep(for: .seconds(5)) | |
throw URLError(.badServerResponse) | |
} | |
} label: { | |
ZStack { | |
Color(uiColor: .systemGray) | |
.frame(width: 96, height: 96) | |
Text("do stuff") | |
} | |
} | |
.buttonStyle(.plain) | |
.disabled(task != .none) | |
.modal(title: Text("some error title"), task: $task) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment