Created
May 3, 2024 08:22
-
-
Save gboyegadada/0bbce795f7e96360548722f0c4f51d93 to your computer and use it in GitHub Desktop.
SwiftUI: Alerting your users or waiting for confirmation from a background thread
This file contains 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
// | |
// 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) | |
} | |
} |
This file contains 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
// | |
// 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?" | |
} | |
} | |
} |
This file contains 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
// | |
// 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()) | |
} | |
} |
This file contains 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
// | |
// AlertResponse.swift | |
// | |
// Created by Gboyega Dada on 03/05/2024. | |
// | |
import Foundation | |
enum AlertResponse: String { | |
case ok | |
case cancel | |
// ... other possible responses | |
} |
This file contains 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
// | |
// 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() | |
} | |
} | |
} |
This file contains 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
// | |
// 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