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:
parent
fe2ee59930
commit
2496cab7f3
3 changed files with 154 additions and 37 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue