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)
This commit is contained in:
Schwifty 2026-03-22 19:20:46 +00:00
parent fe2ee59930
commit 2496cab7f3
3 changed files with 154 additions and 37 deletions

View file

@ -14,19 +14,51 @@ struct BusinessListView: View {
if isLoading { if isLoading {
ProgressView("Loading businesses…") ProgressView("Loading businesses…")
} else if let error = errorMessage { } else if let error = errorMessage {
ContentUnavailableView { if #available(iOS 17.0, *) {
Label("Error", systemImage: "exclamationmark.triangle") ContentUnavailableView {
} description: { Label("Error", systemImage: "exclamationmark.triangle")
Text(error) } description: {
} actions: { Text(error)
Button("Retry") { Task { await loadBusinesses() } } } 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 { } else if businesses.isEmpty {
ContentUnavailableView( if #available(iOS 17.0, *) {
"No Businesses", ContentUnavailableView(
systemImage: "building.2", "No Businesses",
description: Text("You don't have any businesses yet.") 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 { } else {
List(businesses) { business in List(businesses) { business in
Button { Button {

View file

@ -141,18 +141,40 @@ struct QRScannerView: View {
} }
} }
@ViewBuilder
private var cameraPermissionDenied: some View { private var cameraPermissionDenied: some View {
ContentUnavailableView { if #available(iOS 17.0, *) {
Label("Camera Access Required", systemImage: "camera.fill") ContentUnavailableView {
} description: { Label("Camera Access Required", systemImage: "camera.fill")
Text("Open Settings and enable camera access for Payfrit Beacon.") } description: {
} actions: { Text("Open Settings and enable camera access for Payfrit Beacon.")
Button("Open Settings") { } actions: {
if let url = URL(string: UIApplication.openSettingsURLString) { Button("Open Settings") {
UIApplication.shared.open(url) 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()
} }
} }

View file

@ -129,27 +129,62 @@ struct ScanView: View {
.padding() .padding()
} }
@ViewBuilder
private var selectServicePointPrompt: some View { private var selectServicePointPrompt: some View {
ContentUnavailableView( if #available(iOS 17.0, *) {
"Select a Service Point", ContentUnavailableView(
systemImage: "mappin.and.ellipse", "Select a Service Point",
description: Text("Choose or create a service point (table) to provision a beacon for.") 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 // MARK: - Namespace Loading
@ViewBuilder
private var namespaceLoadingView: some View { private var namespaceLoadingView: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
if isLoadingNamespace { if isLoadingNamespace {
ProgressView("Allocating beacon namespace…") ProgressView("Allocating beacon namespace…")
} else { } else {
ContentUnavailableView { if #available(iOS 17.0, *) {
Label("Namespace Error", systemImage: "exclamationmark.triangle") ContentUnavailableView {
} description: { Label("Namespace Error", systemImage: "exclamationmark.triangle")
Text(errorMessage ?? "Failed to allocate beacon namespace") } description: {
} actions: { Text(errorMessage ?? "Failed to allocate beacon namespace")
Button("Retry") { Task { await loadNamespace() } } } 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() .padding()
if bleManager.discoveredBeacons.isEmpty && !bleManager.isScanning { if bleManager.discoveredBeacons.isEmpty && !bleManager.isScanning {
ContentUnavailableView( if #available(iOS 17.0, *) {
"No Beacons Found", ContentUnavailableView(
systemImage: "antenna.radiowaves.left.and.right.slash", "No Beacons Found",
description: Text("Tap Scan to search for nearby beacons") 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 { } else {
List(bleManager.discoveredBeacons) { beacon in List(bleManager.discoveredBeacons) { beacon in
Button { Button {
@ -274,7 +324,7 @@ struct ScanView: View {
Image(systemName: "light.beacon.max") Image(systemName: "light.beacon.max")
.font(.system(size: 64)) .font(.system(size: 64))
.foregroundStyle(.orange) .foregroundStyle(.orange)
.symbolEffect(.pulse) .modifier(PulseEffectModifier())
Text("Beacon is Flashing") Text("Beacon is Flashing")
.font(.title2.bold()) .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
}
}
}