fix build
apple / swift (push) Successful in 55s
ci / rust (push) Successful in 1m16s
ci / web (push) Successful in 33s
ci / docs-site (push) Successful in 29s
android / android (push) Successful in 3m18s
deb / build-publish (push) Successful in 3m7s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
ci / bench (push) Successful in 4m32s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m47s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m50s
docker / deploy-docs (push) Successful in 35s

improve iOS & iPadOS UI
This commit is contained in:
2026-06-19 15:49:48 +02:00
parent 53aade0279
commit 86979d0abc
13 changed files with 192 additions and 19 deletions
+24
View File
@@ -0,0 +1,24 @@
{
"configurations": [
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:punktfunk}/clients/apple",
"name": "Debug PunktfunkClient (clients/apple)",
"target": "PunktfunkClient",
"configuration": "debug",
"preLaunchTask": "swift: Build Debug PunktfunkClient (clients/apple)"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:punktfunk}/clients/apple",
"name": "Release PunktfunkClient (clients/apple)",
"target": "PunktfunkClient",
"configuration": "release",
"preLaunchTask": "swift: Build Release PunktfunkClient (clients/apple)"
}
]
}
@@ -425,6 +425,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = F4H37KF6WC;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Config/Info.plist;
@@ -463,6 +464,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = F4H37KF6WC;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Config/Info.plist;
@@ -500,6 +502,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = F4H37KF6WC;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Config/Info.plist;
@@ -529,6 +532,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = F4H37KF6WC;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Config/Info.plist;
@@ -81,24 +81,50 @@ struct AddHostSheet: View {
#if !os(tvOS)
.formStyle(.grouped)
#endif
#if os(macOS)
// macOS: UNCHANGED Cancel + Spacer + Add in an HStack, both wired to the
// window's default/cancel keyboard actions. The 380-wide .fixedSize panel below
// keeps this compact and centered.
HStack {
Button("Cancel", role: .cancel) { dismiss() }
#if !os(tvOS)
.keyboardShortcut(.cancelAction)
#endif
Spacer()
Button("Add Host") { add() }
.buttonStyle(.borderedProminent)
#if !os(tvOS)
.keyboardShortcut(.defaultAction)
#endif
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
.glassProminentButtonStyle()
.keyboardShortcut(.defaultAction)
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
}
#if os(iOS)
.controlSize(.large)
#endif
.padding(16)
#else
// iOS / iPadOS: NO Cancel the sheet is dismissed by the drag indicator,
// swipe-down, or tap-outside. (AddHostSheet never sets interactiveDismissDisabled,
// so all three are live; if anyone adds it later, restore a Cancel here or there is
// no way back out.) A single FULL-WIDTH primary action reads as the one thing to do.
// The fill must be on the LABEL, not the Button: .frame(maxWidth:.infinity) on the
// Button only widens its hit area and leaves the styled capsule hugging the text
// stretching the label is what makes the glass/bordered pill itself go edge-to-edge.
// .controlSize(.large) gives the tall, thumb-friendly height; .defaultAction lets a
// hardware keyboard / iPad Return submit.
Button { add() } label: {
Text("Add Host").frame(maxWidth: .infinity)
}
.glassProminentButtonStyle()
.controlSize(.large)
.keyboardShortcut(.defaultAction)
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
.padding(16)
#endif
}
#if os(iOS)
// A short bottom sheet, not a full-screen modal. .height(320) hugs the 3-field grouped
// Form + the full-width action row, instead of the half-screen .medium it used to rest
// at. A single fixed detent is enough: the system keeps the content above the keyboard
// when Address/Port is focused, and on iPadOS this renders as a short bottom sheet (not a
// centered formSheet card). If Dynamic Type grows the rows past this height the Form just
// scrolls inside the detent nothing is clipped. (.height(_:) is iOS 16+, safe at iOS 17.)
.presentationDetents([.height(320)])
.presentationDragIndicator(.visible)
#endif
#if os(macOS)
.frame(width: 380)
.fixedSize(horizontal: false, vertical: true)
@@ -205,7 +205,13 @@ struct ContentView: View {
Image(systemName: "xmark")
.font(.headline.weight(.semibold))
.frame(width: 36, height: 36)
.background(.regularMaterial, in: Circle())
// Sole touch exit when the HUD is off a floating glass disc
// over the frame (26+, material fallback). interactive: the disc
// IS the tap target, so the glass reacts to press.
.glassBackground(Circle(), interactive: true)
// Match the hit region to the visible disc so every tap also
// triggers the interactive-glass press highlight.
.contentShape(Circle())
}
.buttonStyle(.plain)
.padding(12)
@@ -0,0 +1,69 @@
// GlassStyle.swift the app's single, availability-gated entry point to Apple's "Liquid
// Glass" (iOS / macOS / tvOS 26). Every Liquid Glass symbol (glassEffect, Glass, the
// .glassProminent button style ) is HARD-gated to OS 26: referencing one with our
// deployment targets (macOS 14 / iOS 17 / tvOS 17) is a COMPILE error, not a silent no-op,
// unless it sits behind `if #available`. So all glass in the app routes through the two
// helpers below, each of which falls back to the EXACT look the app shipped before
// (.regularMaterial / .borderedProminent) nothing regresses on older OSes, and the gating
// lives in exactly one file.
import SwiftUI
// MARK: - Glass background
/// Liquid Glass behind a floating / overlay surface, with the pre-26 `.regularMaterial`
/// look as the fallback. Use ONLY on the floating control / overlay layer (the streaming
/// HUD, the trust card, the touch exit chip) never on content tiles or dense forms (HIG).
///
/// `glassEffect()`'s own default shape is a Capsule, so panels MUST pass an explicit shape
/// (a RoundedRectangle / Circle) or they render as a pill. `interactive` makes the glass
/// react to press only meaningful when the glass itself is the tap target.
private struct GlassBackground<S: Shape>: ViewModifier {
let shape: S
var interactive = false
func body(content: Content) -> some View {
if #available(iOS 26, macOS 26, tvOS 26, *) {
content.glassEffect(interactive ? .regular.interactive() : .regular, in: shape)
} else {
content.background(.regularMaterial, in: shape)
}
}
}
extension View {
/// Liquid Glass (26+) or the existing `.regularMaterial` (pre-26) behind a floating
/// surface. Pass the surface's shape explicitly glass defaults to a Capsule otherwise.
func glassBackground<S: Shape>(_ shape: S, interactive: Bool = false) -> some View {
modifier(GlassBackground(shape: shape, interactive: interactive))
}
}
// MARK: - Glass primary button
/// The single prominent action on a floating / overlay or sheet surface: the Liquid-Glass
/// prominent button style on 26+, falling back to `.borderedProminent` (the app's current
/// primary style) below. Apply directly to a `Button`; role / keyboardShortcut / disabled
/// chain after it as usual. tvOS stays `.borderedProminent` always glass chrome fights the
/// focus engine, and keeping it preserves today's tvOS look exactly.
private struct GlassProminentButton: ViewModifier {
func body(content: Content) -> some View {
#if os(tvOS)
content.buttonStyle(.borderedProminent)
#else
if #available(iOS 26, macOS 26, *) {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
#endif
}
}
extension View {
/// Liquid-Glass prominent style (26+, non-tvOS) or `.borderedProminent`. Drop-in for the
/// `.buttonStyle(.borderedProminent)` on a surface's primary action.
func glassProminentButtonStyle() -> some View {
modifier(GlassProminentButton())
}
}
@@ -217,7 +217,7 @@ struct HomeView: View {
Text("Add your punktfunk host with the + button.")
} actions: {
Button("Add Host") { showAddHost = true }
.buttonStyle(.borderedProminent)
.glassProminentButtonStyle()
#if os(iOS)
.controlSize(.large)
#endif
@@ -88,6 +88,8 @@ struct HostCardView: View {
#if !os(tvOS)
// tvOS: the .card button style owns platter + focus motion extra chrome
// inside it mutes the grow/tilt. Material + accent ring are for pointer UIs.
// Deliberately .regularMaterial, not Liquid Glass: HIG keeps glass off content
// tiles (it flattens hierarchy over an opaque grid) see GlassStyle.swift.
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14))
.overlay {
if isMostRecent {
@@ -81,7 +81,7 @@ struct LibraryView: View {
.foregroundStyle(.secondary)
.frame(maxWidth: 420)
Button("Retry") { Task { await load() } }
.buttonStyle(.borderedProminent)
.glassProminentButtonStyle()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -150,7 +150,7 @@ struct PairSheet: View {
.padding(.trailing, 8)
}
Button("Pair & Connect") { runCeremony() }
.buttonStyle(.borderedProminent)
.glassProminentButtonStyle()
#if !os(tvOS)
.keyboardShortcut(.defaultAction)
#endif
@@ -165,6 +165,15 @@ struct PairSheet: View {
.frame(width: 400)
.fixedSize(horizontal: false, vertical: true)
#endif
#if os(iOS)
// Bottom sheet instead of a full-screen modal (Liquid Glass background on iOS 26).
// .medium rests; .large is included so the sheet grows to keep the Pair/Cancel row
// above the keyboard when the PIN field is focused. Hide the grabber while the ceremony
// is in flight dismissal is disabled then (interactiveDismissDisabled), so a drag
// would only rubber-band; the always-enabled Cancel button is the exit.
.presentationDetents([.medium, .large])
.presentationDragIndicator(busy ? .hidden : .visible)
#endif
.interactiveDismissDisabled(busy)
.onDisappear { token.cancelled = true } // any other dismissal path
#endif
@@ -91,14 +91,14 @@ struct SpeedTestSheet: View {
bitrateKbps = rec
dismiss()
}
.buttonStyle(.borderedProminent)
.glassProminentButtonStyle()
#if !os(tvOS)
.keyboardShortcut(.defaultAction)
#endif
}
if case .failed = phase {
Button("Retry") { run() }
.buttonStyle(.borderedProminent)
.glassProminentButtonStyle()
}
}
}
@@ -112,6 +112,12 @@ struct SpeedTestSheet: View {
.frame(width: 420)
.fixedSize(horizontal: false, vertical: true)
#endif
#if os(iOS)
// Bottom sheet rather than a full-screen modal; .medium stays put as the result view
// swaps in (a measured height would resize the sheet mid-probe).
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
#endif
.onAppear { run() }
.onDisappear { token.cancelled = true }
}
@@ -99,7 +99,9 @@ struct StreamHUDView: View {
#endif
}
.padding(10)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
// Floating HUD over live video the canonical Liquid-Glass overlay surface (26+);
// falls back to .regularMaterial below 26 (see GlassStyle).
.glassBackground(RoundedRectangle(cornerRadius: 10))
.padding(10)
}
}
@@ -38,6 +38,10 @@ struct TrustCardView: View {
.keyboardShortcut(.cancelAction)
#endif
Button("Trust & Connect", action: onTrust)
// Opaque prominent, NOT glass: this card is itself a glass panel
// (.glassBackground below), and glass-on-glass loses contrast a tinted
// bordered button reads cleanly over glass (HIG). The sheet primaries stay
// glass because the system manages the sheet's own glass layering.
.buttonStyle(.borderedProminent)
#if !os(tvOS)
.keyboardShortcut(.defaultAction)
@@ -58,7 +62,9 @@ struct TrustCardView: View {
}
.padding(28)
.frame(maxWidth: 440)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18))
// Floating trust card over the blurred stream Liquid Glass on 26+, .regularMaterial
// fallback below. The inner fingerprint box stays .quaternary (content, not glass).
.glassBackground(RoundedRectangle(cornerRadius: 18))
}
/// 64 hex chars four groups per line, two lines easy to eyeball against the log.
+20 -1
View File
@@ -145,4 +145,23 @@ fi
rm -rf clients/apple/PunktfunkCore.xcframework
"${XCODEBUILD[@]}" -create-xcframework "${ARGS[@]}" -output clients/apple/PunktfunkCore.xcframework
echo "OK: clients/apple/PunktfunkCore.xcframework"
# Xcode (unlike `swift build`) refuses to EMBED an unsigned xcframework: the app targets in
# Punktfunk.xcodeproj fail with "The framework 'PunktfunkCore.xcframework' is unsigned". So
# sign the bundle here. Identity: $CODESIGN_IDENTITY if set, else the first "Apple Development"
# identity in the keychain, else ad-hoc ("-") — ad-hoc satisfies `swift build` and most local
# Xcode runs; a real identity is needed for device/distribution. --timestamp=none keeps it
# offline (a secure timestamp only matters for notarized distribution, which re-signs anyway).
SIGN_ID="${CODESIGN_IDENTITY:-}"
if [[ -z "$SIGN_ID" ]]; then
SIGN_ID=$(security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/Apple Development/ {print $2; exit}')
fi
SIGN_ID="${SIGN_ID:--}" # ad-hoc fallback when no real identity is available
if codesign --force --timestamp=none --sign "$SIGN_ID" clients/apple/PunktfunkCore.xcframework; then
echo "OK: clients/apple/PunktfunkCore.xcframework (signed: $SIGN_ID)"
else
echo "WARN: clients/apple/PunktfunkCore.xcframework built but NOT signed — Xcode app" >&2
echo " builds will report it unsigned. Set CODESIGN_IDENTITY and re-run." >&2
echo "OK: clients/apple/PunktfunkCore.xcframework (unsigned)"
fi