mirror of
https://github.com/home-assistant/iOS.git
synced 2026-06-16 04:16:39 -05:00
## Summary Migrate the notification category list and the category/action editors to SwiftUI: - `NotificationCategoryListView`: local insert/delete, Realm-observed server categories section, navigation to editor. - `NotificationCategoryEditorView`: required fields, server-controlled read-only mode, hidden preview/summary text areas, reorder/insert/delete action rows, YAML service-call preview, toolbar help, preview-notification action. - `NotificationActionEditorView`: required title/identifier, conditional text-input fields, foreground/destructive/authentication toggles, server-controlled read-only mode, YAML trigger preview. Also adds three reusable components: - `YamlPreviewSection` — replaces Eureka `YamlSection` within this slice; generalises the inline version previously in `NFCTagView`. - `NotificationIdentifierField` — SwiftUI text-field helper enforcing identifier casing/validation (replaces `NotificationIdentifierEurekaRow`). - `RealmResultsObserver` — `ObservableObject` wrapping `AnyRealmCollection` with a notification token (reusable by future slices). Deletes the three old `*Configurator.swift` / `*ViewController.swift` files plus `NotificationIdentifierEurekaRow.swift`. ## Screenshots _Pending — to be added before merge._ ## Link to pull request in Documentation repository Documentation: home-assistant/companion.home-assistant# ## Any other notes Part of a five-PR Eureka → SwiftUI migration tracked in `UIKitToSwiftUIMigration.md` (siblings: #4560, #4561, #4562, #4564). The `Eureka`, `ColorPickerRow`, and `ViewRow` pods stay until all slices land. **Reconciliation with #4562:** this PR embeds `NotificationCategoryListView` inside the old `NotificationSettingsViewController` via `UIHostingController` so the list is reachable while the parent screen is still Eureka. Once #4562 merges, `NotificationSettingsView` should link directly to `NotificationCategoryListView` — the hosting-controller wrapper becomes unnecessary. `YamlSection.swift` and `RealmSection.swift` are retained — still used by `ActionConfigurator`, `NFCTagViewController`, `SettingsDetailViewController`, and `ComplicationListViewController`. `bundle exec fastlane lint` passes. Not build-verified locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
3.0 KiB
Swift
84 lines
3.0 KiB
Swift
import SwiftUI
|
|
|
|
/// Validation/formatting helper for notification identifier fields.
|
|
///
|
|
/// Categories and actions require identifiers containing only letters, digits
|
|
/// and underscores. Action identifiers must additionally be uppercase. This
|
|
/// replaces the old Eureka `NotificationIdentifierRow` so the constraints can
|
|
/// be reused from SwiftUI forms.
|
|
enum NotificationIdentifierField {
|
|
/// Sanitises text in-place according to the casing rules. Spaces are
|
|
/// replaced with underscores; any other characters outside the allowed
|
|
/// alphanumeric+underscore set are dropped.
|
|
static func sanitize(_ value: String, uppercaseOnly: Bool) -> String {
|
|
let working = value.replacingOccurrences(of: " ", with: "_")
|
|
let allowed: Set<Character>
|
|
if uppercaseOnly {
|
|
allowed = Set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_")
|
|
} else {
|
|
allowed = Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
|
|
}
|
|
|
|
let filtered = working.filter { allowed.contains($0) }
|
|
if uppercaseOnly {
|
|
return filtered.uppercased()
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
/// True when the sanitized identifier is valid (non-empty, correct casing).
|
|
static func isValid(_ value: String, uppercaseOnly: Bool) -> Bool {
|
|
guard !value.isEmpty else { return false }
|
|
return sanitize(value, uppercaseOnly: uppercaseOnly) == value
|
|
}
|
|
}
|
|
|
|
/// SwiftUI `TextField` wrapper enforcing identifier casing and validation.
|
|
///
|
|
/// - Parameters:
|
|
/// - title: Label shown above or alongside the field by the parent `Form`.
|
|
/// - text: Binding to the sanitized identifier string.
|
|
/// - uppercaseOnly: When true, only `[A-Z0-9_]` characters are allowed.
|
|
/// - isDisabled: Disables editing (used when the identifier is already set).
|
|
struct NotificationIdentifierTextField: View {
|
|
let title: String
|
|
@Binding var text: String
|
|
let uppercaseOnly: Bool
|
|
let isDisabled: Bool
|
|
|
|
init(
|
|
title: String,
|
|
text: Binding<String>,
|
|
uppercaseOnly: Bool,
|
|
isDisabled: Bool = false
|
|
) {
|
|
self.title = title
|
|
self._text = text
|
|
self.uppercaseOnly = uppercaseOnly
|
|
self.isDisabled = isDisabled
|
|
}
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text(title)
|
|
Spacer()
|
|
TextField("", text: Binding(
|
|
get: { text },
|
|
set: { newValue in
|
|
text = NotificationIdentifierField.sanitize(newValue, uppercaseOnly: uppercaseOnly)
|
|
}
|
|
))
|
|
.multilineTextAlignment(.trailing)
|
|
.disableAutocorrection(true)
|
|
.textInputAutocapitalization(uppercaseOnly ? .characters : .never)
|
|
.keyboardType(.asciiCapable)
|
|
.foregroundColor(
|
|
NotificationIdentifierField.isValid(text, uppercaseOnly: uppercaseOnly) || text.isEmpty
|
|
? .primary
|
|
: .red
|
|
)
|
|
.disabled(isDisabled)
|
|
}
|
|
}
|
|
}
|