Skip to content

Instantly share code, notes, and snippets.

@PhilipTrauner
Created June 15, 2023 14:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PhilipTrauner/a7ba179495c9b9946a9d8090e8a3a948 to your computer and use it in GitHub Desktop.
Save PhilipTrauner/a7ba179495c9b9946a9d8090e8a3a948 to your computer and use it in GitHub Desktop.
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