From 2496cab7f31b59f77b19edb4b2e086a37b463cd0 Mon Sep 17 00:00:00 2001 From: Schwifty Date: Sun, 22 Mar 2026 19:20:46 +0000 Subject: [PATCH] fix: wrap iOS 17+ APIs in @available checks for iOS 16 compat ContentUnavailableView and .symbolEffect(.pulse) require iOS 17+ but deployment target is iOS 16. Wrapped all usages in if #available(iOS 17.0, *) with VStack-based fallbacks for iOS 16. Files fixed: - ScanView.swift (4 ContentUnavailableView + 1 symbolEffect) - QRScannerView.swift (1 ContentUnavailableView) - BusinessListView.swift (2 ContentUnavailableView) --- PayfritBeacon/Views/BusinessListView.swift | 54 +++++++++--- PayfritBeacon/Views/QRScannerView.swift | 40 +++++++-- PayfritBeacon/Views/ScanView.swift | 97 ++++++++++++++++++---- 3 files changed, 154 insertions(+), 37 deletions(-) diff --git a/PayfritBeacon/Views/BusinessListView.swift b/PayfritBeacon/Views/BusinessListView.swift index 18365b3..0d4370a 100644 --- a/PayfritBeacon/Views/BusinessListView.swift +++ b/PayfritBeacon/Views/BusinessListView.swift @@ -14,19 +14,51 @@ struct BusinessListView: View { if isLoading { ProgressView("Loading businesses…") } else if let error = errorMessage { - ContentUnavailableView { - Label("Error", systemImage: "exclamationmark.triangle") - } description: { - Text(error) - } actions: { - Button("Retry") { Task { await loadBusinesses() } } + if #available(iOS 17.0, *) { + ContentUnavailableView { + Label("Error", systemImage: "exclamationmark.triangle") + } description: { + Text(error) + } actions: { + Button("Retry") { Task { await loadBusinesses() } } + } + } else { + VStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 48)) + .foregroundStyle(.orange) + Text("Error") + .font(.headline) + Text(error) + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + Button("Retry") { Task { await loadBusinesses() } } + .buttonStyle(.borderedProminent) + } + .padding() } } else if businesses.isEmpty { - ContentUnavailableView( - "No Businesses", - systemImage: "building.2", - description: Text("You don't have any businesses yet.") - ) + if #available(iOS 17.0, *) { + ContentUnavailableView( + "No Businesses", + systemImage: "building.2", + description: Text("You don't have any businesses yet.") + ) + } else { + VStack(spacing: 12) { + Image(systemName: "building.2") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text("No Businesses") + .font(.headline) + Text("You don't have any businesses yet.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + .frame(maxHeight: .infinity) + .padding() + } } else { List(businesses) { business in Button { diff --git a/PayfritBeacon/Views/QRScannerView.swift b/PayfritBeacon/Views/QRScannerView.swift index b82db83..e33158e 100644 --- a/PayfritBeacon/Views/QRScannerView.swift +++ b/PayfritBeacon/Views/QRScannerView.swift @@ -141,18 +141,40 @@ struct QRScannerView: View { } } + @ViewBuilder private var cameraPermissionDenied: some View { - ContentUnavailableView { - Label("Camera Access Required", systemImage: "camera.fill") - } description: { - Text("Open Settings and enable camera access for Payfrit Beacon.") - } actions: { - Button("Open Settings") { - if let url = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(url) + if #available(iOS 17.0, *) { + ContentUnavailableView { + Label("Camera Access Required", systemImage: "camera.fill") + } description: { + Text("Open Settings and enable camera access for Payfrit Beacon.") + } actions: { + Button("Open Settings") { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } } + .buttonStyle(.borderedProminent) } - .buttonStyle(.borderedProminent) + } else { + VStack(spacing: 16) { + Image(systemName: "camera.fill") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text("Camera Access Required") + .font(.headline) + Text("Open Settings and enable camera access for Payfrit Beacon.") + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + Button("Open Settings") { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + } + .buttonStyle(.borderedProminent) + } + .padding() } } diff --git a/PayfritBeacon/Views/ScanView.swift b/PayfritBeacon/Views/ScanView.swift index 86bfc41..61b9062 100644 --- a/PayfritBeacon/Views/ScanView.swift +++ b/PayfritBeacon/Views/ScanView.swift @@ -129,27 +129,62 @@ struct ScanView: View { .padding() } + @ViewBuilder private var selectServicePointPrompt: some View { - ContentUnavailableView( - "Select a Service Point", - systemImage: "mappin.and.ellipse", - description: Text("Choose or create a service point (table) to provision a beacon for.") - ) + if #available(iOS 17.0, *) { + ContentUnavailableView( + "Select a Service Point", + systemImage: "mappin.and.ellipse", + description: Text("Choose or create a service point (table) to provision a beacon for.") + ) + } else { + VStack(spacing: 12) { + Image(systemName: "mappin.and.ellipse") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text("Select a Service Point") + .font(.headline) + Text("Choose or create a service point (table) to provision a beacon for.") + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxHeight: .infinity) + .padding() + } } // MARK: - Namespace Loading + @ViewBuilder private var namespaceLoadingView: some View { VStack(spacing: 16) { if isLoadingNamespace { ProgressView("Allocating beacon namespace…") } else { - ContentUnavailableView { - Label("Namespace Error", systemImage: "exclamationmark.triangle") - } description: { - Text(errorMessage ?? "Failed to allocate beacon namespace") - } actions: { - Button("Retry") { Task { await loadNamespace() } } + if #available(iOS 17.0, *) { + ContentUnavailableView { + Label("Namespace Error", systemImage: "exclamationmark.triangle") + } description: { + Text(errorMessage ?? "Failed to allocate beacon namespace") + } actions: { + Button("Retry") { Task { await loadNamespace() } } + } + } else { + VStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 48)) + .foregroundStyle(.orange) + Text("Namespace Error") + .font(.headline) + Text(errorMessage ?? "Failed to allocate beacon namespace") + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + Button("Retry") { Task { await loadNamespace() } } + .buttonStyle(.borderedProminent) + } + .padding() } } } @@ -245,11 +280,26 @@ struct ScanView: View { .padding() if bleManager.discoveredBeacons.isEmpty && !bleManager.isScanning { - ContentUnavailableView( - "No Beacons Found", - systemImage: "antenna.radiowaves.left.and.right.slash", - description: Text("Tap Scan to search for nearby beacons") - ) + if #available(iOS 17.0, *) { + ContentUnavailableView( + "No Beacons Found", + systemImage: "antenna.radiowaves.left.and.right.slash", + description: Text("Tap Scan to search for nearby beacons") + ) + } else { + VStack(spacing: 12) { + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text("No Beacons Found") + .font(.headline) + Text("Tap Scan to search for nearby beacons") + .font(.subheadline) + .foregroundStyle(.secondary) + } + .frame(maxHeight: .infinity) + .padding() + } } else { List(bleManager.discoveredBeacons) { beacon in Button { @@ -274,7 +324,7 @@ struct ScanView: View { Image(systemName: "light.beacon.max") .font(.system(size: 64)) .foregroundStyle(.orange) - .symbolEffect(.pulse) + .modifier(PulseEffectModifier()) Text("Beacon is Flashing") .font(.title2.bold()) @@ -768,3 +818,16 @@ struct BeaconRow: View { } } +// MARK: - iOS 16/17 Compatibility + +/// Applies `.symbolEffect(.pulse)` on iOS 17+, no-op on iOS 16 +private struct PulseEffectModifier: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + content.symbolEffect(.pulse) + } else { + content + } + } +} +