Files
vnidrop/apple/VniDrop/UI/Components/AdaptiveDrawer.swift
cdricms ef42875ddb feat(apple): generate type-safe L10n accessors and migrate all key literals
Replaces every stringly-typed localization key in the Apple app with
compile-time-checked accessors generated from localization/strings.json.
A mistyped key is now a build error instead of a silent fallback to the
raw key at runtime. The runtime path is unchanged: plain keys are
String.LocalizationValue constants resolved with String(localized:) and
Apple's String Catalog still does the lookup; keys with arguments become
typed, named functions applying args through String(format:).

Generator: new renderSwiftAccessors emits apple/VniDrop/Generated/L10n.swift,
wired into generate. Renamed generic arg1/arg2 tokens on four keys to
semantic names (receiver, transferName, deviceId) and updated their context
notes; positional output is unchanged so .xcstrings (bar the 4 comments)
and the Android XML regenerate identical.

Migration: every key-carrying value flipped to String.LocalizationValue
end to end, resolved only at the leaf. Zero key literals and zero
LocalizedStringKey remain in app or test code. macOS build passes; iOS
test run pending.
2026-07-23 17:12:43 +02:00

67 lines
1.8 KiB
Swift

import SwiftUI
/// Presents modal content in a native sheet. On phones it uses medium/large
/// detents with a grabber; on wider layouts the sheet is form-sized. Content is
/// wrapped in a `NavigationStack` so it gets a native title bar + Close button.
struct AdaptiveDrawer<DrawerContent: View>: ViewModifier {
@Binding var isPresented: Bool
let windowClass: WindowClass
let onDismiss: () -> Void
@ViewBuilder let drawerContent: () -> DrawerContent
func body(content: Content) -> some View {
content.sheet(
isPresented: Binding(get: { isPresented }, set: { if !$0 { onDismiss() } })
) {
SheetChrome(onClose: onDismiss) { drawerContent() }
.modifier(PhoneDetents(enabled: windowClass == .phone))
}
}
}
private struct PhoneDetents: ViewModifier {
let enabled: Bool
func body(content: Content) -> some View {
if enabled {
content
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
} else {
content.frame(minWidth: 460, minHeight: 480)
}
}
}
private struct SheetChrome<Content: View>: View {
let onClose: () -> Void
@ViewBuilder let content: () -> Content
var body: some View {
NavigationStack {
ScrollView { content().padding(.top, 4) }
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String(localized: L10n.Button.close), action: onClose)
}
}
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
}
extension View {
func adaptiveDrawer<DrawerContent: View>(
isPresented: Binding<Bool>,
windowClass: WindowClass,
onDismiss: @escaping () -> Void,
@ViewBuilder content: @escaping () -> DrawerContent
) -> some View {
modifier(AdaptiveDrawer(
isPresented: isPresented, windowClass: windowClass,
onDismiss: onDismiss, drawerContent: content
))
}
}