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,6 +14,7 @@ struct BusinessListView: View {
if isLoading { if isLoading {
ProgressView("Loading businesses…") ProgressView("Loading businesses…")
} else if let error = errorMessage { } else if let error = errorMessage {
if #available(iOS 17.0, *) {
ContentUnavailableView { ContentUnavailableView {
Label("Error", systemImage: "exclamationmark.triangle") Label("Error", systemImage: "exclamationmark.triangle")
} description: { } description: {
@ -21,12 +22,43 @@ struct BusinessListView: View {
} actions: { } actions: {
Button("Retry") { Task { await loadBusinesses() } } 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 {
if #available(iOS 17.0, *) {
ContentUnavailableView( ContentUnavailableView(
"No Businesses", "No Businesses",
systemImage: "building.2", systemImage: "building.2",
description: Text("You don't have any businesses yet.") 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,7 +141,9 @@ struct QRScannerView: View {
} }
} }
@ViewBuilder
private var cameraPermissionDenied: some View { private var cameraPermissionDenied: some View {
if #available(iOS 17.0, *) {
ContentUnavailableView { ContentUnavailableView {
Label("Camera Access Required", systemImage: "camera.fill") Label("Camera Access Required", systemImage: "camera.fill")
} description: { } description: {
@ -154,6 +156,26 @@ struct QRScannerView: View {
} }
.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()
}
} }
// MARK: - Actions // MARK: - Actions

View file

@ -129,21 +129,40 @@ struct ScanView: View {
.padding() .padding()
} }
@ViewBuilder
private var selectServicePointPrompt: some View { private var selectServicePointPrompt: some View {
if #available(iOS 17.0, *) {
ContentUnavailableView( ContentUnavailableView(
"Select a Service Point", "Select a Service Point",
systemImage: "mappin.and.ellipse", systemImage: "mappin.and.ellipse",
description: Text("Choose or create a service point (table) to provision a beacon for.") 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 {
if #available(iOS 17.0, *) {
ContentUnavailableView { ContentUnavailableView {
Label("Namespace Error", systemImage: "exclamationmark.triangle") Label("Namespace Error", systemImage: "exclamationmark.triangle")
} description: { } description: {
@ -151,6 +170,22 @@ struct ScanView: View {
} actions: { } actions: {
Button("Retry") { Task { await loadNamespace() } } 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()
}
} }
} }
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
@ -245,11 +280,26 @@ struct ScanView: View {
.padding() .padding()
if bleManager.discoveredBeacons.isEmpty && !bleManager.isScanning { if bleManager.discoveredBeacons.isEmpty && !bleManager.isScanning {
if #available(iOS 17.0, *) {
ContentUnavailableView( ContentUnavailableView(
"No Beacons Found", "No Beacons Found",
systemImage: "antenna.radiowaves.left.and.right.slash", systemImage: "antenna.radiowaves.left.and.right.slash",
description: Text("Tap Scan to search for nearby beacons") 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
}
}
}