Files
element-x-ios/ElementX/Sources/Other/Extensions/Alert.swift
T
Mauro Romito 26c417ae68 Move AlertInfo's button types out of the generic type
With Swift 6.4, the default value for AlertInfo.primaryButton warned that
T's Hashable conformance may be isolated and can't be used in a main
actor-isolated context. The nested AlertButton/AlertTextField types
don't depend on T, so move them to top level as AlertInfoButton and
AlertInfoTextField, and keep typealiases so existing call sites compile.

Constraining T to SendableMetatype isn't possible as the synthesized
Hashable conformances of our main actor-isolated alert enums are isolated.
2026-10-01 10:55:40 +02:00

146 lines
5.4 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftUI
protocol AlertProtocol {
var title: String { get }
}
extension View {
func alert<Item: AlertProtocol, Actions: View, Message: View>(item: Binding<Item?>, @ViewBuilder actions: (Item) -> Actions, @ViewBuilder message: (Item) -> Message) -> some View {
let binding = Binding<Bool>(get: {
item.wrappedValue != nil
}, set: { newValue in
if !newValue {
item.wrappedValue = nil
}
})
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message)
}
// periphery: ignore - not used yet but might be useful
func alert<Item: AlertProtocol, Actions: View>(item: Binding<Item?>, @ViewBuilder actions: (Item) -> Actions) -> some View {
let binding = Binding<Bool>(get: {
item.wrappedValue != nil
}, set: { newValue in
if !newValue {
item.wrappedValue = nil
}
})
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions)
}
}
/// A type that describes an alert to be shown to the user.
///
/// The alert info can be added to the view state bindings and used as an alert's `item`:
/// ```
/// view
/// .alert(item: $context.alertInfo)
/// ```
struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
// The button types live outside this generic type, otherwise the default
// primaryButton would need T's (possibly isolated) Hashable conformance.
typealias AlertButton = AlertInfoButton
typealias AlertTextField = AlertInfoTextField
/// An identifier that can be used to distinguish one error from another.
let id: T
/// The alert's title.
let title: String
/// The alert's message (optional).
var message: String?
/// The alert's primary button title and action. Defaults to an Ok button with no action.
var primaryButton = AlertButton(title: L10n.actionOk, action: nil)
/// The alert's secondary button title and action.
var secondaryButton: AlertButton?
/// The alert's displayed text fields.
var textFields: [AlertTextField]?
/// The alert's additional buttons displayed vertically above the primary button.
var verticalButtons: [AlertButton]?
}
extension AlertInfo {
/// Initialises the type with a generic title and message for an unknown error along with the default Ok button.
/// - Parameters:
/// - id: An ID that identifies the error.
/// - error: The Error that occurred.
init(id: T) {
self.id = id
title = L10n.commonError
message = L10n.errorUnknown
}
// periphery: ignore - might be useful in the future
/// Initialises the type with the title from an `Error`'s localised description along with the default Ok button.
///
/// Currently this initialiser creates an alert for every error, however in the future it may be updated to filter
/// out some specific errors such as cancellation and networking issues that create too much noise or are
/// indicated to the user using other mechanisms.
init(error: Error) where T == String {
self.init(id: error.localizedDescription,
title: error.localizedDescription)
}
}
struct AlertInfoButton: Identifiable {
let id = UUID()
let title: String
var role: ButtonRole?
let action: (() -> Void)?
}
struct AlertInfoTextField: Identifiable {
let id = UUID()
let placeholder: String
let text: Binding<String>
let autoCapitalization: TextInputAutocapitalization
let autoCorrectionDisabled: Bool
}
extension View {
func alert<T: Hashable>(item: Binding<AlertInfo<T>?>) -> some View {
alert(item: item) { item in
if let verticalButtons = item.verticalButtons {
ForEach(verticalButtons) { button in
Button(button.title, role: button.role) {
button.action?()
}
}
}
if let textFields = item.textFields {
VStack(spacing: 24) {
ForEach(textFields) { textField in
TextField(textField.placeholder, text: textField.text)
.textInputAutocapitalization(textField.autoCapitalization)
.autocorrectionDisabled(textField.autoCorrectionDisabled)
}
}
}
Button(item.primaryButton.title, role: item.primaryButton.role) {
item.primaryButton.action?()
}
.accessibilityIdentifier(A11yIdentifiers.alertInfo.primaryButton)
if let secondaryButton = item.secondaryButton {
Button(secondaryButton.title, role: secondaryButton.role) {
secondaryButton.action?()
}
.accessibilityIdentifier(A11yIdentifiers.alertInfo.secondaryButton)
}
} message: { item in
if let message = item.message {
Text(message)
}
}
}
}