Files
iOS/Sources/App/Settings/Components/NotificationIdentifierField.swift
Bruno Pantaleão Gonçalves 814152e5c3 Migrate Notification category/action editors to SwiftUI (#4563)
## 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>
2026-04-29 02:16:27 +02:00

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)
}
}
}