Skip to content

Instantly share code, notes, and snippets.

@gboyegadada
Created May 3, 2024 08:22
Show Gist options
  • Save gboyegadada/0bbce795f7e96360548722f0c4f51d93 to your computer and use it in GitHub Desktop.
Save gboyegadada/0bbce795f7e96360548722f0c4f51d93 to your computer and use it in GitHub Desktop.
SwiftUI: Alerting your users or waiting for confirmation from a background thread
//
// AlertCenter.swift
//
// Created by Gboyega Dada on 03/05/2024.
//
import Foundation
import SwiftUI
@Observable class AlertCenter {
public static let shared: AlertCenter = .init()
var isPresenting: Bool = false
private(set) var error: AlertError = .unknownError
@ObservationIgnored
private var continuation: AsyncStream<AlertResponse>.Continuation?
@ObservationIgnored
lazy private var responseStream: AsyncStream<AlertResponse> = AsyncStream(AlertResponse.self) { [weak self] continuation in
self?.continuation = continuation
}
public func present(_ type: AlertError) async -> AlertResponse {
await MainActor.run {
self.error = type
self.isPresenting = true
}
for await response in responseStream {
return response
}
return .cancel
}
@MainActor public func dismissed(with response: AlertResponse = .cancel) {
self.error = .unknownError
Task(priority: .userInitiated) {
self.continuation?.yield(response)
}
}
}
private struct AlertCenterKey: EnvironmentKey {
static let defaultValue: AlertCenter = .shared
}
extension EnvironmentValues {
var alertCenter: AlertCenter {
get { self[AlertCenterKey.self] }
set { self[AlertCenterKey.self] = newValue }
}
}
extension View {
func alertCenter(_ instance: AlertCenter) -> some View {
environment(\.alertCenter, instance)
}
}
//
// AlertError.swift
//
// Created by Gboyega Dada on 03/05/2024.
//
enum AlertError: LocalizedError {
case unknownError
case confirmDeleteAllData
}
// MARK: - Error Description
extension AlertError {
var errorDescription: String? {
switch self {
case .unknownError:
"Unknown Error"
case .confirmDeleteAllData:
"Account Change"
}
}
var failureReason: String {
switch self {
case .unknownError:
"Unknown Error"
case .confirmDeleteAllData:
"All of your saved content will be deleted. Continue?"
}
}
}
//
// AlertModifier.swift
//
// Created by Gboyega Dada on 03/05/2024.
//
import Foundation
import SwiftUI
struct AlertModifier: ViewModifier {
@Environment(\.alertCenter) private var alertCenter: AlertCenter
func body(content: Content) -> some View {
let isPresented = Binding(
get: { alertCenter.isPresenting },
set: { alertCenter.isPresenting = $0 }
)
content
.alert(isPresented: isPresented, error: alertCenter.error) { error in
Button("Ok") {
alertCenter.dismissed(with: .ok)
}
Button("Cancel", role: .cancel) {
alertCenter.dismissed(with: .cancel)
}
} message: { error in
Text(error.failureReason)
}
}
}
extension View {
func myAppAlerts() -> some View {
modifier(AlertModifier())
}
}
//
// AlertResponse.swift
//
// Created by Gboyega Dada on 03/05/2024.
//
import Foundation
enum AlertResponse: String {
case ok
case cancel
// ... other possible responses
}
//
// ExamplApp.swift
//
// Created by Gboyega Dada on 30/03/2024.
//
import SwiftUI
@main
struct ExampleApp: App {
let database = SyncedDatabase()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.database)
.myAppAlerts()
}
}
}
//
// SyncedDatabase.swift
// SyncEngine
//
// This is just an example of a situation in which you might want to confirm an
// action in your background logic. To see the full sample CloudKit example...
//
// @See https://github.com/apple/sample-cloudkit-sync-engine/tree/main
//
import CloudKit
import Foundation
import os.log
final actor SyncedDatabase : Sendable, ObservableObject {
// ...
func handleAccountChange(_ event: CKSyncEngine.Event.AccountChange) {
// Handling account changes can be tricky.
//
// If the user signed out of their account, we want to delete all local data.
// However, what if there's some data that hasn't been uploaded yet?
// Should we keep that data? Prompt the user to keep the data? Or just delete it?
//
// Also, what if the user signs in to a new account, and there's already some data locally?
// Should we upload it to their account? Or should we delete it?
//
// Finally, what if the user signed in, but they were signed into a previous account before?
//
// Since we're in a sample app, we're going to take a relatively simple approach.
let shouldDeleteLocalData: Bool
let shouldReUploadLocalData: Bool
switch event.changeType {
// ...
case .switchAccounts:
shouldDeleteLocalData = true
shouldReUploadLocalData = false
case .signOut:
shouldDeleteLocalData = true
shouldReUploadLocalData = false
// ...
}
if shouldDeleteLocalData {
let response = await AlertCenter.shared.present(.confirmDeleteAllData)
if response == .ok {
try? self.deleteLocalData() // This error should be handled, but we'll skip that for brevity in this sample app.
}
}
if shouldReUploadLocalData {
let recordZoneChanges: [CKSyncEngine.PendingRecordZoneChange] = self.appData.contacts.values.map { .saveRecord($0.recordID) }
self.syncEngine.state.add(pendingDatabaseChanges: [ .saveZone(CKRecordZone(zoneName: Contact.zoneName)) ])
self.syncEngine.state.add(pendingRecordZoneChanges: recordZoneChanges)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment