diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
new file mode 100644
index 0000000..7420032
--- /dev/null
+++ b/.github/workflows/swift.yml
@@ -0,0 +1,56 @@
+name: Test Swift Package on iOS
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ name: Test Swift 6.0 on iOS (${{ matrix.config }})
+ runs-on: macos-latest # iOS testing requires macOS
+
+ strategy:
+ matrix:
+ config: [debug, release]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Select Xcode (latest)
+ uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: 'latest'
+
+ - name: Setup Swift 6.0
+ uses: SwiftyLab/setup-swift@latest
+ with:
+ swift-version: '6.0' # Use quotes to ensure correct version parsing
+
+ - name: Check Swift version
+ run: swift --version
+
+ - name: Find an iOS Simulator
+ id: find_simulator
+ run: |
+ # Find an available iOS simulator runtime and device
+ RUNTIME_ID=$(xcrun simctl list runtimes ios --json | jq -r '.runtimes[0].identifier')
+ if [ -z "$RUNTIME_ID" ] || [ "$RUNTIME_ID" == "null" ]; then
+ echo "::error::No iOS runtime found."
+ exit 1
+ fi
+ DEVICE_ID=$(xcrun simctl list devices --json | jq -r --arg RT_ID "$RUNTIME_ID" '.devices[$RT_ID] | map(select(.isAvailable)) | .[0].udid')
+ if [ -z "$DEVICE_ID" ] || [ "$DEVICE_ID" == "null" ]; then
+ echo "::error::No available iOS simulator device found for runtime $RUNTIME_ID."
+ exit 1
+ fi
+ echo "Found iOS Simulator Runtime: $RUNTIME_ID"
+ echo "Found iOS Simulator Device UDID: $DEVICE_ID"
+ echo "SIMULATOR_DESTINATION=platform=iOS Simulator,id=$DEVICE_ID" >> $GITHUB_OUTPUT
+
+ - name: Build and Test (${{ matrix.config }})
+ run: |
+ echo "Using simulator destination: ${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}"
+ xcodebuild build -scheme NotchMyProblem -sdk $(xcrun --sdk iphonesimulator --show-sdk-path) -destination "${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}" SWIFT_VERSION=6.0
diff --git a/Examples/NotchMyProblemDemo/ContentView.swift b/Examples/NotchMyProblemDemo/ContentView.swift
index 51ea81e..ab23021 100644
--- a/Examples/NotchMyProblemDemo/ContentView.swift
+++ b/Examples/NotchMyProblemDemo/ContentView.swift
@@ -74,14 +74,14 @@ struct ContentView: View {
.padding()
// Buttons positioned around the notch/island
- TopologyButtonsView(
- leadingButton: {
+ CutoutAccessoryView(
+ leadingContent: {
Button(action: { }) {
Image(systemName: "arrow.left")
.modifier(ButtonStyleModifier())
}
},
- trailingButton: {
+ trailingContent: {
Button(action: { }) {
Text("Done")
.modifier(ButtonStyleModifier())
diff --git a/README.md b/README.md
index bd23efc..34c4bf4 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,18 @@
Swift package that handles the annoying task of positioning UI elements around the iPhone's notch and Dynamic Island
- Compatible with iOS 13.0 and later
+
+
+
+
Custom Overrides for API Inaccuracies
Some devices report incorrect notch dimensions through the API. Overrides correct the reported values to match actual device dimensions, ensuring consistent UI across all devices.
-NotchMyProblem provides several ways to customize how the notch/island area is handled:
-
-#### 1. Global Overrides (App-wide)
+### 1. Global Overrides (App-wide)
```swift
-// In your App's initialization
+// In your App’s initialization (e.g. in @main or AppDelegate)
NotchMyProblem.globalOverrides = [
.series(prefix: "iPhone13", scale: 0.95, heightFactor: 1.0, radius: 27),
DeviceOverride(modelIdentifier: "iPhone14,3", scale: 0.8, heightFactor: 0.7)
]
```
-#### 2. Instance Overrides
+### 2. Instance Overrides
```swift
-// For specific use cases
+// At runtime, for specific cases
NotchMyProblem.shared.overrides = [
DeviceOverride(modelIdentifier: "iPhone14,3", scale: 0.8, heightFactor: 0.7)
]
```
-#### 3. View-Specific Overrides (using SwiftUI modifiers)
-
-```swift
-TopologyButtonsView(
- leadingButton: { /* ... */ },
- trailingButton: { /* ... */ }
-)
-.notchOverride(.series(prefix: "iPhone14", scale: 0.6, heightFactor: 0.6))
-```
-
-### Override Precedence
-
-Overrides are applied in the following order (highest priority first):
-1. View-specific overrides (via `.notchOverride()` modifier)
-2. Instance-specific exact model matches
-3. Instance-specific series matches
-4. Global exact model matches
-5. Global series matches
-
----
-
-## **Creating Device Overrides**
-
-### For Specific Device Models
+### 3. View-Specific Overrides (SwiftUI)
```swift
-// For a specific device model
-let override = DeviceOverride(
- modelIdentifier: "iPhone14,3", // Exact model
- scale: 0.8, // Width scale (0.8 = 80% of original width)
- heightFactor: 0.7, // Height scale (0.7 = 70% of original height)
- radius: 24 // Corner radius (for visualization)
-)
+CutoutAccessoryView(/* … */)
+ .notchOverride(.series(prefix: "iPhone14", scale: 0.6, heightFactor: 0.6))
```
-### For Device Series
+#### Override Precedence
-```swift
-// For all devices in a series
-let seriesOverride = DeviceOverride.series(
- prefix: "iPhone14", // All iPhone 14 models
- scale: 0.75, // Width scale
- heightFactor: 0.75, // Height scale
- radius: 24 // Corner radius
-)
-```
+1. View-specific overrides
+2. Instance-specific exact model
+3. Instance-specific series prefix
+4. Global exact model
+5. Global series prefix
---
## **Manual Access**
-If you need direct access to the notch/island dimensions:
-
```swift
-// Get the raw exclusion rect (unmodified)
-let rawRect = NotchMyProblem.exclusionRect
-
-// Get the adjusted rect with any applicable overrides
-let adjustedRect = NotchMyProblem.shared.adjustedExclusionRect
-
-// Get a custom-adjusted rect with specific overrides
+let rawRect = NotchMyProblem.exclusionRect // raw API result
+let adjusted = NotchMyProblem.shared.adjustedExclusionRect // with global/instance overrides
let customRect = NotchMyProblem.shared.adjustedExclusionRect(using: myOverrides)
```
@@ -169,57 +201,55 @@ let customRect = NotchMyProblem.shared.adjustedExclusionRect(using: myOverrides)
## **How It Works**
-NotchMyProblem uses a safe approach to access the device's notch/Dynamic Island information:
-
-1. It retrieves the exclusion area using Objective-C runtime features
-2. It safely checks for the existence of methods before calling them
-3. It applies device-specific adjustments based on the model identifier
-4. It provides fallbacks if the information cannot be retrieved
-
-The package is designed to be robust against API changes and includes comprehensive logging to help diagnose any issues.
+1. Uses Objective-C runtime to safely fetch the exclusion area
+2. Falls back gracefully if the API is unavailable
+3. Applies device-specific scale/height overrides
+4. Provides SwiftUI modifiers and environment overrides for fine-grained control
+5. Includes logging (iOS 14+ `Logger`, iOS 13 `os_log`)
---
## **Compatibility**
-- Requires iOS 13.0 or later
-- Supports all notched iPhones (X, XS, XR, 11, 12, 13 series)
-- Supports Dynamic Island devices (iPhone 14 Pro and newer)
-- Safely falls back on devices without notches
+- iOS 13.0+
+- All notched iPhones (X → 14, 16e…)
+- Dynamic Island devices (14 Pro, newer)
+- Fallback for devices without cutouts
---
## **Logging**
-NotchMyProblem includes built-in logging that works across iOS versions:
-- Uses `Logger` on iOS 14+
-- Falls back to `os_log` on iOS 13
-- Provides helpful debug information
-
-To see logs, filter Console app output with subsystem: `com.notchmyproblem`
+Filter Console with subsystem `com.notchmyproblem` to see debug/info/error logs.
---
-## License
-
-This project is released under the MIT License. See [LICENSE](LICENSE.md) for details.
+## **License**
+MIT — see [LICENSE.md](LICENSE.md)
-## Contributing
+## **Contributing**
-Contributions are welcome! Please feel free to submit a Pull Request. Before you begin, take a moment to review the [Contributing Guide](CONTRIBUTING.md) for details on issue reporting, coding standards, and the PR process.
+Please review [CONTRIBUTING.md](CONTRIBUTING.md) before opening PRs.
-## Support
+## **Support**
-If you like this project, please consider giving it a ⭐️
+If you like it, please give a ⭐️
---
# Acknowledgments
-- This package uses private API information in a safe, non-invasive way, however use at your own risk considering app store rules
-- Check out [TopNotch](https://github.com/samhenrigold/TopNotch) which helped inspire this solution and provided valuable insights into working with the notch/Dynamic Island
+- Inspired by [TopNotch](https://github.com/samhenrigold/TopNotch)
+- Uses private APIs safely—use at your own risk
---
+## Where to find me:
+- here, obviously.
+- [Twitter](https://x.com/AetherAurelia)
+- [Threads](https://www.threads.net/@aetheraurelia)
+- [Bluesky](https://bsky.app/profile/aethers.world)
+- [LinkedIn](https://www.linkedin.com/in/willjones24)
+
Built with 🍏📱🏝️ by Aether
diff --git a/Sources/NotchMyProblem/CutoutAccessoryView.swift b/Sources/NotchMyProblem/CutoutAccessoryView.swift
new file mode 100644
index 0000000..b27a281
--- /dev/null
+++ b/Sources/NotchMyProblem/CutoutAccessoryView.swift
@@ -0,0 +1,280 @@
+//
+// CutoutAccessoryView.swift
+// NotchMyProblem
+//
+// Created by Aether on 03/03/2025.
+//
+
+import SwiftUI
+
+/// Padding configuration for `CutoutAccessoryView`.
+///
+/// - `.auto`: Uses default heuristics for cutout, content, and vertical padding.
+/// - The cutout area gets `cutoutWidth / 8` horizontal padding.
+/// - The overall content gets `cutoutWidth / 4` horizontal padding.
+/// - The vertical padding is `cutoutHeight * 0.05`.
+///
+/// - `.none`: No extra padding is applied to the cutout, content, or vertically.
+///
+/// - `.custom`: Supply closures to calculate the horizontal padding for the cutout area,
+/// the overall content, and the vertical padding. Each closure receives the relevant
+/// cutout dimension (in points) and should return the desired padding (in points).
+///
+/// Example:
+/// ```swift
+/// CutoutAccessoryView(
+/// padding: .custom(
+/// cutout: { cutoutWidth in cutoutWidth / 10 }, // Horizontal padding for the cutout area
+/// content: { cutoutWidth in cutoutWidth / 2 }, // Horizontal padding for the overall content
+/// vertical: { cutoutHeight in cutoutHeight * 0.1 } // Vertical padding (default is 5% of cutout height)
+/// ),
+/// leadingContent: { ... },
+/// trailingContent: { ... }
+/// )
+/// ```
+///
+/// - `cutout`: Closure to determine the horizontal padding for the cutout's surroundings (the space reserved for the notch/island).
+/// - `content`: Closure to determine the horizontal padding for the overall content (the HStack containing your views).
+/// - `vertical`: Closure to determine the vertical padding for the content (default is `{ $0 * 0.05 }`).
+public enum CutoutAccessoryPadding {
+ case auto
+ case none
+ case custom(
+ cutout: (CGFloat) -> CGFloat,
+ content: (CGFloat) -> CGFloat,
+ vertical: (CGFloat) -> CGFloat = { $0 * 0.05 }
+ )
+}
+
+/// A view that positions content around the physical topology of the device's top area,
+/// adapting to notches, Dynamic Islands, and other screen cutouts automatically.
+///
+/// You can provide any views for the leading and trailing sides.
+///
+/// - Parameters:
+/// - padding: The padding configuration for the cutout and content area. Default is `.auto`.
+/// - leadingContent: The view to display on the left side.
+/// - trailingContent: The view to display on the right side.
+@available(iOS 13.0, *)
+public struct CutoutAccessoryView
: View {
+ // Environment access to any custom overrides
+ @Environment(\.notchOverrides) private var environmentOverrides
+
+ // The view that appears on the left/leading side
+ let leadingContent: LeadingContent
+
+ // The view that appears on the right/trailing side
+ let trailingContent: TrailingContent
+
+ // Padding configuration
+ let padding: CutoutAccessoryPadding
+
+ // Access class for device topology
+ let notchMyProblem = NotchMyProblem.self
+
+ /// Creates a new CutoutAccessoryView with custom leading and trailing content.
+ /// - Parameters:
+ /// - padding: The padding configuration for the cutout and content area. Default is `.auto`.
+ /// - leadingContent: The view to display on the left side.
+ /// - trailingContent: The view to display on the right side.
+ public init(
+ padding: CutoutAccessoryPadding = .auto,
+ @ViewBuilder leadingContent: () -> LeadingContent,
+ @ViewBuilder trailingContent: () -> TrailingContent
+ ) {
+ self.padding = padding
+ self.leadingContent = leadingContent()
+ self.trailingContent = trailingContent()
+ }
+
+ public var body: some View {
+ GeometryReader { geometry in
+ // Detect device topology based on safe area height
+ let statusBarHeight = geometry.safeAreaInsets.top
+ let hasTopCutout = statusBarHeight > 40
+
+ let exclusionWidth = getAdjustedExclusionRect()?.width ?? geometry.size.width * 0.4
+
+ let exclusionHeight = notchMyProblem.exclusionRect?.height ?? 0
+
+ let (cutoutPadding, contentPadding, verticalPadding): (CGFloat, CGFloat, CGFloat) = {
+ switch padding {
+ case .auto:
+ return (exclusionWidth / 6, exclusionWidth / (3), exclusionHeight * 0.1)
+ case .none:
+ return (0, 0, 0)
+ case .custom(let cutout, let content, let vertical):
+ return (cutout(exclusionWidth), content(exclusionWidth), vertical(exclusionHeight))
+ }
+ }()
+
+ HStack(spacing: 0) {
+ // Leading content, aligned appropriately
+ leadingContent
+ .frame(maxWidth: .infinity, alignment: hasTopCutout ? .center : .leading)
+
+ // Space for the device's top cutout if present
+ if exclusionWidth > 0 {
+ Color.clear
+ .frame(width: exclusionWidth)
+ .padding(.horizontal, cutoutPadding)
+ }
+
+ // Trailing content, aligned appropriately
+ trailingContent
+ .frame(maxWidth: .infinity, alignment: hasTopCutout ? .center : .trailing)
+ }
+ .padding(.vertical, verticalPadding)
+ .frame(height: hasTopCutout ? notchMyProblem.exclusionRect?.height ?? statusBarHeight : 30)
+ .padding(.top, notchMyProblem.exclusionRect?.minY ?? (hasTopCutout ? 0 : 5))
+ .padding(.horizontal, hasTopCutout ? contentPadding : 5)
+ .edgesIgnoringSafeArea(.all)
+ }
+ }
+
+ /// Gets the adjusted exclusion rect, applying any environment overrides
+ private func getAdjustedExclusionRect() -> CGRect? {
+ if let overrides = environmentOverrides {
+ // Use environment-specific overrides if available
+ return notchMyProblem.shared.adjustedExclusionRect(using: overrides)
+ } else {
+ // Otherwise use the instance's configured overrides
+ return notchMyProblem.shared.adjustedExclusionRect
+ }
+ }
+}
+
+// MARK: - Previews
+
+#Preview("Default (.auto)") {
+ ZStack {
+ NavigationView {
+ ScrollView {
+ VStack(alignment: .leading) {
+ Text("Recommended for most cases. Adds horizontal padding to both the cutout area and the overall content, and vertical padding based on the cutout height. Ensures content doesn't touch the notch, device corners, or crowd the top edge.")
+ .font(.subheadline)
+ .padding(.horizontal)
+ }
+ }
+ .navigationBarTitle("Default (.auto) Padding", displayMode: .inline)
+ }
+ CutoutAccessoryView(
+ padding: .auto,
+ leadingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Leading").font(.footnote).foregroundColor(.white))
+ },
+ trailingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Trailing").font(.footnote).foregroundColor(.white))
+ }
+ )
+ }
+}
+
+#Preview("No Padding (.none)") {
+ ZStack {
+ NavigationView {
+ ScrollView {
+ VStack(alignment: .leading) {
+ Text("No extra padding is applied. Content may touch the notch, device corners, or top edge. Use for full-bleed designs or when you want to manage spacing yourself.")
+ .font(.subheadline)
+ .padding(.horizontal)
+ }
+ }
+ .navigationBarTitle("No Padding (.none)", displayMode: .inline)
+ }
+ CutoutAccessoryView(
+ padding: .none,
+ leadingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Leading").foregroundColor(.white))
+ },
+ trailingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Trailing").foregroundColor(.white))
+ }
+ )
+ }
+}
+
+#Preview("Custom Padding (.custom)") {
+ ZStack {
+ NavigationView {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Custom math for cutout, content, and vertical padding.")
+ .font(.subheadline)
+
+ Divider()
+
+ VStack(alignment: .leading, spacing: 12) {
+ Text("The cutout area receives horizontal padding equal to 1/12 of the cutout’s width.")
+ Text("The overall content receives horizontal padding equal to 1/6 of the cutout’s width.")
+ Text("Vertical padding is set to 20% of the cutout’s height.")
+ }
+ .font(.subheadline)
+ }
+ .padding(.horizontal)
+ }
+ .navigationBarTitle("Custom (.custom)", displayMode: .inline)
+ }
+ CutoutAccessoryView(
+ padding: .custom(
+ cutout: { $0 / 12 },
+ content: { $0 / 6 },
+ vertical: { $0 * 0.2 }
+ ),
+ leadingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Leading").foregroundColor(.white))
+ },
+ trailingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Trailing").foregroundColor(.white))
+ }
+ )
+ }
+}
+
+#Preview("With View Override (iPhone 14)") {
+ ZStack {
+ NavigationView {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("On some devices, the system-reported cutout information can be inaccurate.")
+ .font(.subheadline)
+
+ Divider()
+
+ VStack(alignment: .leading, spacing: 12) {
+ Text("This preview demonstrates using a view-specific override to ensure correct cutout handling on iPhone 14 models.")
+ Text("(Overrides are provided by default and can be customized—see documentation.)")
+ }
+ .font(.subheadline)
+ }
+ .padding(.horizontal)
+ }
+ .navigationBarTitle("Override (iPhone 14)", displayMode: .inline)
+ }
+ CutoutAccessoryView(
+ leadingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Leading").foregroundColor(.white))
+ },
+ trailingContent: {
+ Capsule()
+ .fill(.red)
+ .overlay(Text("Trailing").foregroundColor(.white))
+ }
+ )
+ .notchOverride(.series(prefix: "iPhone14", scale: 0.6, heightFactor: 0.6))
+ }
+}
diff --git a/Sources/NotchMyProblem/NotchMyProblem.swift b/Sources/NotchMyProblem/NotchMyProblem.swift
index 28415ef..9cb971e 100644
--- a/Sources/NotchMyProblem/NotchMyProblem.swift
+++ b/Sources/NotchMyProblem/NotchMyProblem.swift
@@ -1,3 +1,392 @@
-// The Swift Programming Language
-// https://docs.swift.org/swift-book
+//
+// Classes.swift
+// NotchMyProblem
+//
+// Created by Aether on 03/03/2025.
+//
+import SwiftUI
+import os
+import UIKit
+
+/// Extension to safely access the exclusion area (notch/Dynamic Island)
+extension UIScreen {
+ /// Returns the frame of the Dynamic Island or notch.
+ var exclusionArea: CGRect? {
+ // Early return for devices known not to have a notch/island
+ // Check if the device is an iPhone and if it has a notch based on model
+ let modelId = UIDevice.modelIdentifier
+ let isNotchedDevice = modelId.hasPrefix("iPhone") &&
+ !["iPhone8", "iPhone9", "iPhone10,4", "iPhone10,5"].contains { modelId.hasPrefix($0) }
+
+ if !isNotchedDevice {
+ return nil
+ }
+
+ let areaExclusionSelector = {
+ let selectorName = ["Area", "exclusion", "_"].reversed().joined()
+ return NSSelectorFromString(selectorName)
+ }()
+
+ // Check if the method exists before trying to call it
+ guard self.responds(to: areaExclusionSelector) else {
+ return nil
+ }
+
+ // Safely get the exclusion area object
+ let areaExclusionMethod = {
+ let implementation = self.method(for: areaExclusionSelector)
+ let methodType = (@convention(c) (AnyObject, Selector) -> AnyObject?).self
+ return unsafeBitCast(implementation, to: methodType)
+ }()
+
+ let object = areaExclusionMethod(self, areaExclusionSelector)
+
+ // Check if the object exists and responds to the rect selector
+ let rectSelector = NSSelectorFromString("rect")
+ guard let object, object.responds(to: rectSelector) else {
+ return nil
+ }
+
+ let rectMethod = {
+ let implementation = object.method(for: rectSelector)
+ let methodType = (@convention(c) (AnyObject, Selector) -> CGRect).self
+ return unsafeBitCast(implementation, to: methodType)
+ }()
+
+ let rect = rectMethod(object, rectSelector)
+
+ // Validate the rect before returning it
+ if rect.width <= 0 || rect.height <= 0 || rect.isInfinite || rect.isNull {
+ return nil
+ }
+
+ return rect
+ }
+}
+
+
+/// Logging helper that works across iOS versions
+@available(iOS 13.0, *)
+struct NMPLogger {
+ private let subsystem = "com.notchmyproblem"
+ private let category: String
+
+ #if os(iOS)
+ @available(iOS 14.0, *)
+ private var logger: Logger {
+ Logger(subsystem: subsystem, category: category)
+ }
+
+ private var osLog: OSLog {
+ OSLog(subsystem: subsystem, category: category)
+ }
+ #endif
+
+ init(category: String) {
+ self.category = category
+ }
+
+ func debug(_ message: String) {
+ #if os(iOS)
+ if #available(iOS 14.0, *) {
+ logger.debug("\(message)")
+ } else {
+ os_log("%{public}@", log: osLog, type: .debug, message)
+ }
+ #endif
+ }
+
+ func info(_ message: String) {
+ #if os(iOS)
+ if #available(iOS 14.0, *) {
+ logger.info("\(message)")
+ } else {
+ os_log("%{public}@", log: osLog, type: .info, message)
+ }
+ #endif
+ }
+
+ func notice(_ message: String) {
+ #if os(iOS)
+ if #available(iOS 14.0, *) {
+ logger.notice("\(message)")
+ } else {
+ os_log("%{public}@", log: osLog, type: .default, message)
+ }
+ #endif
+ }
+
+ func error(_ message: String) {
+ #if os(iOS)
+ if #available(iOS 14.0, *) {
+ logger.error("\(message)")
+ } else {
+ os_log("%{public}@", log: osLog, type: .error, message)
+ }
+ #endif
+ }
+}
+
+/// Configuration for a device-specific notch/island adjustment
+public struct DeviceOverride: Equatable, Hashable, Sendable {
+ /// The device model identifier or prefix to match
+ public let modelIdentifier: String
+
+ /// Scale factor to apply to the width (1.0 = original width)
+ public let scale: CGFloat
+
+ /// Factor to apply to the height (1.0 = original height)
+ public let heightFactor: CGFloat
+
+ /// Corner radius (if needed for visualization)
+ public let radius: CGFloat
+
+ /// Whether this is an exact match or a prefix match
+ public let isExactMatch: Bool
+
+ /// Creates a new device override with the specified parameters
+ /// - Parameters:
+ /// - modelIdentifier: The device model to match (e.g., "iPhone14,3")
+ /// - scale: Width scale factor (default: 1.0)
+ /// - heightFactor: Height scale factor (default: 1.0)
+ /// - radius: Corner radius (default: 0)
+ /// - isExactMatch: Whether to match the exact model or use as prefix (default: true)
+ public init(
+ modelIdentifier: String,
+ scale: CGFloat = 1.0,
+ heightFactor: CGFloat = 1.0,
+ radius: CGFloat = 0,
+ isExactMatch: Bool = true
+ ) {
+ self.modelIdentifier = modelIdentifier
+ self.scale = scale
+ self.heightFactor = heightFactor
+ self.radius = radius
+ self.isExactMatch = isExactMatch
+ }
+
+ /// Creates a series override that matches any device whose model ID starts with the prefix
+ /// - Parameters:
+ /// - seriesPrefix: The device series prefix (e.g., "iPhone14")
+ /// - scale: Width scale factor
+ /// - heightFactor: Height scale factor
+ /// - radius: Corner radius
+ public static func series(
+ prefix: String,
+ scale: CGFloat,
+ heightFactor: CGFloat,
+ radius: CGFloat = 0
+ ) -> DeviceOverride {
+ DeviceOverride(
+ modelIdentifier: prefix,
+ scale: scale,
+ heightFactor: heightFactor,
+ radius: radius,
+ isExactMatch: false
+ )
+ }
+}
+
+/// Manages the detection and adjustment of the iPhone's top notch/Dynamic Island area
+@available(iOS 13.0, *)
+@MainActor
+public final class NotchMyProblem: Sendable, ObservableObject {
+
+ // MARK: - Logging
+
+ /// Logger for NotchMyProblem class
+ private static let logger = NMPLogger(category: "NotchMyProblem")
+
+ // MARK: - Singleton
+
+ /// Shared instance for app-wide access
+ public static let shared = NotchMyProblem()
+
+ // MARK: - Properties
+
+ /// Current device model identifier
+ let modelId = UIDevice.modelIdentifier
+
+ // MARK: - Notch Detection
+
+ /// The raw notch/Dynamic Island area retrieved via private API
+ /// Uses a safer approach to access private APIs
+ public static let exclusionRect: CGRect? = {
+ if let rect = UIScreen.main.exclusionArea {
+ logger.info("Found notch for \(UIDevice.modelIdentifier): \(rect)")
+ return rect
+ }
+
+ logger.notice("No notch found, returning .zero")
+ return nil
+ }()
+
+ // MARK: - Device-specific Adjustments
+
+ /// Global overrides that apply to all instances
+ public static var globalOverrides: [DeviceOverride] = [
+ .series(prefix: "iPhone13", scale: 0.95, heightFactor: 1.0, radius: 27), // iPhone 12 series
+ .series(prefix: "iPhone14", scale: 0.75, heightFactor: 0.75, radius: 24) // iPhone 13/14 series
+ ]
+
+ /// Instance-specific overrides that only apply to this instance
+ @Published public var overrides: [DeviceOverride] = []
+
+ // MARK: - Initialization
+
+ /// Private initializer to enforce singleton pattern
+ private init() {
+ // This class is isolated to the main actor since it interacts with UIKit
+ }
+
+ // MARK: - Public API
+
+ /// Returns the notch/island area with device-specific adjustments applied
+ /// Use this for proper UI positioning around the top cutout
+ public var adjustedExclusionRect: CGRect? {
+ adjustedExclusionRect(using: overrides)
+ }
+
+ /// Returns the notch/island area with the specified overrides applied
+ /// - Parameter customOverrides: Custom overrides to use for this specific calculation
+ /// - Returns: The adjusted exclusion rect
+ public func adjustedExclusionRect(using customOverrides: [DeviceOverride]? = nil) -> CGRect? {
+ let baseRect = NotchMyProblem.exclusionRect
+
+ guard let baseRect else { return nil }
+
+ // Determine which overrides to use (in order of precedence)
+ let effectiveOverrides = customOverrides ?? overrides
+
+ // Try instance overrides first (exact matches)
+ for override in effectiveOverrides where override.isExactMatch && override.modelIdentifier == modelId {
+ let adjusted = applyOverride(to: baseRect, with: override)
+ NotchMyProblem.logger.debug("Applied instance exact override for \(self.modelId): \(adjusted)")
+ return adjusted
+ }
+
+ // Then try instance overrides (prefix matches)
+ for override in effectiveOverrides where !override.isExactMatch && modelId.hasPrefix(override.modelIdentifier) {
+ let adjusted = applyOverride(to: baseRect, with: override)
+ NotchMyProblem.logger.debug("Applied instance series override \(override.modelIdentifier) for \(self.modelId): \(adjusted)")
+ return adjusted
+ }
+
+ // Then try global overrides (exact matches)
+ for override in NotchMyProblem.globalOverrides where override.isExactMatch && override.modelIdentifier == modelId {
+ let adjusted = applyOverride(to: baseRect, with: override)
+ NotchMyProblem.logger.debug("Applied global exact override for \(self.modelId): \(adjusted)")
+ return adjusted
+ }
+
+ // Finally try global overrides (prefix matches)
+ for override in NotchMyProblem.globalOverrides where !override.isExactMatch && modelId.hasPrefix(override.modelIdentifier) {
+ let adjusted = applyOverride(to: baseRect, with: override)
+ NotchMyProblem.logger.debug("Applied global series override \(override.modelIdentifier) for \(self.modelId): \(adjusted)")
+ return adjusted
+ }
+
+ // When in doubt, use what we found
+ NotchMyProblem.logger.debug("No overrides applied for \(self.modelId)")
+ return baseRect
+ }
+
+ // MARK: - Private Helpers
+
+ /// Applies the specified override parameters to adjust the notch rect
+ private func applyOverride(to rect: CGRect, with override: DeviceOverride) -> CGRect {
+ // Scale the width
+ let scaledWidth = rect.width * override.scale
+
+ // Adjust the height
+ let scaledHeight = rect.height * override.heightFactor
+
+ // Keep it centered
+ let originX = rect.origin.x + (rect.width - scaledWidth) / 2
+
+ // Build the adjusted rect
+ return CGRect(x: originX, y: rect.origin.y, width: scaledWidth, height: scaledHeight)
+ }
+}
+
+/// Device identification utilities
+extension UIDevice {
+ /// Logger for UIDevice extension
+ private static let logger = NMPLogger(category: "UIDeviceExtension")
+
+ /// The device's model identifier (e.g., "iPhone14,4")
+ @MainActor
+ static let modelIdentifier: String = {
+ // Handle simulator case
+ if let simulatorModelIdentifier = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] {
+ logger.debug("Running in simulator with model: \(simulatorModelIdentifier)")
+ return simulatorModelIdentifier
+ }
+
+ // Get actual device identifier
+ var sysinfo = utsname()
+ uname(&sysinfo)
+ let machineData = Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN))
+ let identifier = String(bytes: machineData, encoding: .ascii)?
+ .trimmingCharacters(in: .controlCharacters) ?? "unknown"
+
+ logger.debug("Device model identified as: \(identifier)")
+ return identifier
+ }()
+}
+
+/// SwiftUI view extension for applying custom notch overrides
+@available(iOS 13.0, *)
+public extension View {
+ /// Applies custom notch/island overrides to this view hierarchy
+ /// - Parameter overrides: The device overrides to apply
+ /// - Returns: A view with the specified notch overrides
+ func notchOverrides(_ overrides: [DeviceOverride]) -> some View {
+ modifier(NotchOverrideModifier(overrides: overrides))
+ }
+ func notchOverrides(_ overrides: [DeviceOverride]?) -> some View {
+ modifier(NotchOverrideModifier(overrides: overrides.map { $0 } ?? []))
+ }
+
+ /// Applies a single custom notch/island override to this view hierarchy
+ /// - Parameter override: The device override to apply
+ /// - Returns: A view with the specified notch override
+ func notchOverride(_ override: DeviceOverride) -> some View {
+ notchOverrides([override])
+ }
+
+ /// Conditionally applies a custom notch/island override to this view hierarchy
+ /// - Parameter override: Optional device override to apply (nil means no override)
+ /// - Returns: A view with the specified notch override if provided
+ func notchOverride(_ override: DeviceOverride?) -> some View {
+ modifier(NotchOverrideModifier(overrides: override.map { [$0] } ?? []))
+ }
+}
+
+/// Environment key for notch overrides
+@available(iOS 13.0, *)
+private struct NotchOverridesKey: EnvironmentKey {
+ static let defaultValue: [DeviceOverride]? = nil
+}
+
+/// Environment extension for notch overrides
+@available(iOS 13.0, *)
+public extension EnvironmentValues {
+ /// Custom notch overrides for the current environment
+ var notchOverrides: [DeviceOverride]? {
+ get { self[NotchOverridesKey.self] }
+ set { self[NotchOverridesKey.self] = newValue }
+ }
+}
+
+/// View modifier for applying notch overrides
+@available(iOS 13.0, *)
+private struct NotchOverrideModifier: ViewModifier {
+ @ObservedObject private var notchManager = NotchMyProblem.shared
+ let overrides: [DeviceOverride]
+
+ func body(content: Content) -> some View {
+ content.environment(\.notchOverrides, overrides)
+ }
+}
diff --git a/Sources/NotchMyProblem/NotchMyProblemClass.swift b/Sources/NotchMyProblem/NotchMyProblemClass.swift
deleted file mode 100644
index 9cb971e..0000000
--- a/Sources/NotchMyProblem/NotchMyProblemClass.swift
+++ /dev/null
@@ -1,392 +0,0 @@
-//
-// Classes.swift
-// NotchMyProblem
-//
-// Created by Aether on 03/03/2025.
-//
-
-import SwiftUI
-import os
-import UIKit
-
-/// Extension to safely access the exclusion area (notch/Dynamic Island)
-extension UIScreen {
- /// Returns the frame of the Dynamic Island or notch.
- var exclusionArea: CGRect? {
- // Early return for devices known not to have a notch/island
- // Check if the device is an iPhone and if it has a notch based on model
- let modelId = UIDevice.modelIdentifier
- let isNotchedDevice = modelId.hasPrefix("iPhone") &&
- !["iPhone8", "iPhone9", "iPhone10,4", "iPhone10,5"].contains { modelId.hasPrefix($0) }
-
- if !isNotchedDevice {
- return nil
- }
-
- let areaExclusionSelector = {
- let selectorName = ["Area", "exclusion", "_"].reversed().joined()
- return NSSelectorFromString(selectorName)
- }()
-
- // Check if the method exists before trying to call it
- guard self.responds(to: areaExclusionSelector) else {
- return nil
- }
-
- // Safely get the exclusion area object
- let areaExclusionMethod = {
- let implementation = self.method(for: areaExclusionSelector)
- let methodType = (@convention(c) (AnyObject, Selector) -> AnyObject?).self
- return unsafeBitCast(implementation, to: methodType)
- }()
-
- let object = areaExclusionMethod(self, areaExclusionSelector)
-
- // Check if the object exists and responds to the rect selector
- let rectSelector = NSSelectorFromString("rect")
- guard let object, object.responds(to: rectSelector) else {
- return nil
- }
-
- let rectMethod = {
- let implementation = object.method(for: rectSelector)
- let methodType = (@convention(c) (AnyObject, Selector) -> CGRect).self
- return unsafeBitCast(implementation, to: methodType)
- }()
-
- let rect = rectMethod(object, rectSelector)
-
- // Validate the rect before returning it
- if rect.width <= 0 || rect.height <= 0 || rect.isInfinite || rect.isNull {
- return nil
- }
-
- return rect
- }
-}
-
-
-/// Logging helper that works across iOS versions
-@available(iOS 13.0, *)
-struct NMPLogger {
- private let subsystem = "com.notchmyproblem"
- private let category: String
-
- #if os(iOS)
- @available(iOS 14.0, *)
- private var logger: Logger {
- Logger(subsystem: subsystem, category: category)
- }
-
- private var osLog: OSLog {
- OSLog(subsystem: subsystem, category: category)
- }
- #endif
-
- init(category: String) {
- self.category = category
- }
-
- func debug(_ message: String) {
- #if os(iOS)
- if #available(iOS 14.0, *) {
- logger.debug("\(message)")
- } else {
- os_log("%{public}@", log: osLog, type: .debug, message)
- }
- #endif
- }
-
- func info(_ message: String) {
- #if os(iOS)
- if #available(iOS 14.0, *) {
- logger.info("\(message)")
- } else {
- os_log("%{public}@", log: osLog, type: .info, message)
- }
- #endif
- }
-
- func notice(_ message: String) {
- #if os(iOS)
- if #available(iOS 14.0, *) {
- logger.notice("\(message)")
- } else {
- os_log("%{public}@", log: osLog, type: .default, message)
- }
- #endif
- }
-
- func error(_ message: String) {
- #if os(iOS)
- if #available(iOS 14.0, *) {
- logger.error("\(message)")
- } else {
- os_log("%{public}@", log: osLog, type: .error, message)
- }
- #endif
- }
-}
-
-/// Configuration for a device-specific notch/island adjustment
-public struct DeviceOverride: Equatable, Hashable, Sendable {
- /// The device model identifier or prefix to match
- public let modelIdentifier: String
-
- /// Scale factor to apply to the width (1.0 = original width)
- public let scale: CGFloat
-
- /// Factor to apply to the height (1.0 = original height)
- public let heightFactor: CGFloat
-
- /// Corner radius (if needed for visualization)
- public let radius: CGFloat
-
- /// Whether this is an exact match or a prefix match
- public let isExactMatch: Bool
-
- /// Creates a new device override with the specified parameters
- /// - Parameters:
- /// - modelIdentifier: The device model to match (e.g., "iPhone14,3")
- /// - scale: Width scale factor (default: 1.0)
- /// - heightFactor: Height scale factor (default: 1.0)
- /// - radius: Corner radius (default: 0)
- /// - isExactMatch: Whether to match the exact model or use as prefix (default: true)
- public init(
- modelIdentifier: String,
- scale: CGFloat = 1.0,
- heightFactor: CGFloat = 1.0,
- radius: CGFloat = 0,
- isExactMatch: Bool = true
- ) {
- self.modelIdentifier = modelIdentifier
- self.scale = scale
- self.heightFactor = heightFactor
- self.radius = radius
- self.isExactMatch = isExactMatch
- }
-
- /// Creates a series override that matches any device whose model ID starts with the prefix
- /// - Parameters:
- /// - seriesPrefix: The device series prefix (e.g., "iPhone14")
- /// - scale: Width scale factor
- /// - heightFactor: Height scale factor
- /// - radius: Corner radius
- public static func series(
- prefix: String,
- scale: CGFloat,
- heightFactor: CGFloat,
- radius: CGFloat = 0
- ) -> DeviceOverride {
- DeviceOverride(
- modelIdentifier: prefix,
- scale: scale,
- heightFactor: heightFactor,
- radius: radius,
- isExactMatch: false
- )
- }
-}
-
-/// Manages the detection and adjustment of the iPhone's top notch/Dynamic Island area
-@available(iOS 13.0, *)
-@MainActor
-public final class NotchMyProblem: Sendable, ObservableObject {
-
- // MARK: - Logging
-
- /// Logger for NotchMyProblem class
- private static let logger = NMPLogger(category: "NotchMyProblem")
-
- // MARK: - Singleton
-
- /// Shared instance for app-wide access
- public static let shared = NotchMyProblem()
-
- // MARK: - Properties
-
- /// Current device model identifier
- let modelId = UIDevice.modelIdentifier
-
- // MARK: - Notch Detection
-
- /// The raw notch/Dynamic Island area retrieved via private API
- /// Uses a safer approach to access private APIs
- public static let exclusionRect: CGRect? = {
- if let rect = UIScreen.main.exclusionArea {
- logger.info("Found notch for \(UIDevice.modelIdentifier): \(rect)")
- return rect
- }
-
- logger.notice("No notch found, returning .zero")
- return nil
- }()
-
- // MARK: - Device-specific Adjustments
-
- /// Global overrides that apply to all instances
- public static var globalOverrides: [DeviceOverride] = [
- .series(prefix: "iPhone13", scale: 0.95, heightFactor: 1.0, radius: 27), // iPhone 12 series
- .series(prefix: "iPhone14", scale: 0.75, heightFactor: 0.75, radius: 24) // iPhone 13/14 series
- ]
-
- /// Instance-specific overrides that only apply to this instance
- @Published public var overrides: [DeviceOverride] = []
-
- // MARK: - Initialization
-
- /// Private initializer to enforce singleton pattern
- private init() {
- // This class is isolated to the main actor since it interacts with UIKit
- }
-
- // MARK: - Public API
-
- /// Returns the notch/island area with device-specific adjustments applied
- /// Use this for proper UI positioning around the top cutout
- public var adjustedExclusionRect: CGRect? {
- adjustedExclusionRect(using: overrides)
- }
-
- /// Returns the notch/island area with the specified overrides applied
- /// - Parameter customOverrides: Custom overrides to use for this specific calculation
- /// - Returns: The adjusted exclusion rect
- public func adjustedExclusionRect(using customOverrides: [DeviceOverride]? = nil) -> CGRect? {
- let baseRect = NotchMyProblem.exclusionRect
-
- guard let baseRect else { return nil }
-
- // Determine which overrides to use (in order of precedence)
- let effectiveOverrides = customOverrides ?? overrides
-
- // Try instance overrides first (exact matches)
- for override in effectiveOverrides where override.isExactMatch && override.modelIdentifier == modelId {
- let adjusted = applyOverride(to: baseRect, with: override)
- NotchMyProblem.logger.debug("Applied instance exact override for \(self.modelId): \(adjusted)")
- return adjusted
- }
-
- // Then try instance overrides (prefix matches)
- for override in effectiveOverrides where !override.isExactMatch && modelId.hasPrefix(override.modelIdentifier) {
- let adjusted = applyOverride(to: baseRect, with: override)
- NotchMyProblem.logger.debug("Applied instance series override \(override.modelIdentifier) for \(self.modelId): \(adjusted)")
- return adjusted
- }
-
- // Then try global overrides (exact matches)
- for override in NotchMyProblem.globalOverrides where override.isExactMatch && override.modelIdentifier == modelId {
- let adjusted = applyOverride(to: baseRect, with: override)
- NotchMyProblem.logger.debug("Applied global exact override for \(self.modelId): \(adjusted)")
- return adjusted
- }
-
- // Finally try global overrides (prefix matches)
- for override in NotchMyProblem.globalOverrides where !override.isExactMatch && modelId.hasPrefix(override.modelIdentifier) {
- let adjusted = applyOverride(to: baseRect, with: override)
- NotchMyProblem.logger.debug("Applied global series override \(override.modelIdentifier) for \(self.modelId): \(adjusted)")
- return adjusted
- }
-
- // When in doubt, use what we found
- NotchMyProblem.logger.debug("No overrides applied for \(self.modelId)")
- return baseRect
- }
-
- // MARK: - Private Helpers
-
- /// Applies the specified override parameters to adjust the notch rect
- private func applyOverride(to rect: CGRect, with override: DeviceOverride) -> CGRect {
- // Scale the width
- let scaledWidth = rect.width * override.scale
-
- // Adjust the height
- let scaledHeight = rect.height * override.heightFactor
-
- // Keep it centered
- let originX = rect.origin.x + (rect.width - scaledWidth) / 2
-
- // Build the adjusted rect
- return CGRect(x: originX, y: rect.origin.y, width: scaledWidth, height: scaledHeight)
- }
-}
-
-/// Device identification utilities
-extension UIDevice {
- /// Logger for UIDevice extension
- private static let logger = NMPLogger(category: "UIDeviceExtension")
-
- /// The device's model identifier (e.g., "iPhone14,4")
- @MainActor
- static let modelIdentifier: String = {
- // Handle simulator case
- if let simulatorModelIdentifier = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] {
- logger.debug("Running in simulator with model: \(simulatorModelIdentifier)")
- return simulatorModelIdentifier
- }
-
- // Get actual device identifier
- var sysinfo = utsname()
- uname(&sysinfo)
- let machineData = Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN))
- let identifier = String(bytes: machineData, encoding: .ascii)?
- .trimmingCharacters(in: .controlCharacters) ?? "unknown"
-
- logger.debug("Device model identified as: \(identifier)")
- return identifier
- }()
-}
-
-/// SwiftUI view extension for applying custom notch overrides
-@available(iOS 13.0, *)
-public extension View {
- /// Applies custom notch/island overrides to this view hierarchy
- /// - Parameter overrides: The device overrides to apply
- /// - Returns: A view with the specified notch overrides
- func notchOverrides(_ overrides: [DeviceOverride]) -> some View {
- modifier(NotchOverrideModifier(overrides: overrides))
- }
- func notchOverrides(_ overrides: [DeviceOverride]?) -> some View {
- modifier(NotchOverrideModifier(overrides: overrides.map { $0 } ?? []))
- }
-
- /// Applies a single custom notch/island override to this view hierarchy
- /// - Parameter override: The device override to apply
- /// - Returns: A view with the specified notch override
- func notchOverride(_ override: DeviceOverride) -> some View {
- notchOverrides([override])
- }
-
- /// Conditionally applies a custom notch/island override to this view hierarchy
- /// - Parameter override: Optional device override to apply (nil means no override)
- /// - Returns: A view with the specified notch override if provided
- func notchOverride(_ override: DeviceOverride?) -> some View {
- modifier(NotchOverrideModifier(overrides: override.map { [$0] } ?? []))
- }
-}
-
-/// Environment key for notch overrides
-@available(iOS 13.0, *)
-private struct NotchOverridesKey: EnvironmentKey {
- static let defaultValue: [DeviceOverride]? = nil
-}
-
-/// Environment extension for notch overrides
-@available(iOS 13.0, *)
-public extension EnvironmentValues {
- /// Custom notch overrides for the current environment
- var notchOverrides: [DeviceOverride]? {
- get { self[NotchOverridesKey.self] }
- set { self[NotchOverridesKey.self] = newValue }
- }
-}
-
-/// View modifier for applying notch overrides
-@available(iOS 13.0, *)
-private struct NotchOverrideModifier: ViewModifier {
- @ObservedObject private var notchManager = NotchMyProblem.shared
- let overrides: [DeviceOverride]
-
- func body(content: Content) -> some View {
- content.environment(\.notchOverrides, overrides)
- }
-}
diff --git a/Sources/NotchMyProblem/TopologyButtonsView.swift b/Sources/NotchMyProblem/TopologyButtonsView.swift
deleted file mode 100644
index 41b1987..0000000
--- a/Sources/NotchMyProblem/TopologyButtonsView.swift
+++ /dev/null
@@ -1,154 +0,0 @@
-//
-// TopologyButtonsView.swift
-// NotchMyProblem
-//
-// Created by Aether on 03/03/2025.
-//
-
-import SwiftUI
-
-/// A view that positions buttons around the physical topology of the device's top area,
-/// adapting to notches, Dynamic Islands, and other screen cutouts automatically.
-@available(iOS 13.0, *)
-public struct TopologyButtonsView: View {
- // The button that appears on the left/leading side
- let leadingButton: LeadingButton
-
- // The button that appears on the right/trailing side
- let trailingButton: TrailingButton
-
- // Environment access to any custom overrides
- @Environment(\.notchOverrides) private var environmentOverrides
-
- /// Creates a new TopologyButtonsView with custom leading and trailing buttons
- /// - Parameters:
- /// - leadingButton: The button to display on the left side
- /// - trailingButton: The button to display on the right side
- public init(
- @ViewBuilder leadingButton: () -> LeadingButton,
- @ViewBuilder trailingButton: () -> TrailingButton
- ) {
- self.leadingButton = leadingButton()
- self.trailingButton = trailingButton()
- }
-
- public var body: some View {
- GeometryReader { geometry in
- // Detect device topology based on safe area height
- let statusBarHeight = geometry.safeAreaInsets.top
- let hasTopCutout = statusBarHeight > 40
-
- HStack(spacing: 0) {
- // Leading button with appropriate alignment
- leadingButton
- .frame(maxWidth: .infinity, alignment: hasTopCutout ? .center : .leading)
-
- .padding(7)
-
- // Space for the device's top cutout if present
- if hasTopCutout, let exclusionWidth = getAdjustedExclusionRect()?.width, exclusionWidth > 0 {
- Color.clear
- .frame(width: exclusionWidth)
- }
-
- // Trailing button with appropriate alignment
- trailingButton
- .frame(maxWidth: .infinity, alignment: hasTopCutout ? .center : .trailing)
- .padding(7)
- }
- // Adjust height based on device topology
- .frame(height: hasTopCutout ? statusBarHeight + 4 : 40)
- .padding(.top, hasTopCutout ? 0 : 5)
- .edgesIgnoringSafeArea(.all)
- .padding(.horizontal, 15)
- }
- }
-
- /// Gets the adjusted exclusion rect, applying any environment overrides
- private func getAdjustedExclusionRect() -> CGRect? {
- if let overrides = environmentOverrides {
- // Use environment-specific overrides if available
- let rect = NotchMyProblem.shared.adjustedExclusionRect(using: overrides)
- return rect
- } else {
- // Otherwise use the instance's configured overrides
- let rect = NotchMyProblem.shared.adjustedExclusionRect
- return rect
- }
- }
-}
-
-#Preview {
- // Default TopologyButtonsView
- TopologyButtonsView(
- leadingButton: {
- Button(action: {
- print("Default: Back tapped")
- }) {
- Image(systemName: "chevron.left")
- .font(.headline)
- }
- },
- trailingButton: {
- Button(action: {
- print("Default: Save tapped")
- }) {
- Text("Save")
- .font(.headline)
- }
- }
- )
- .previewDisplayName("Default")
-
-}
-
-#Preview {
-
-
- // TopologyButtonsView with view-specific override
- TopologyButtonsView(
- leadingButton: {
- Button(action: {
- print("Override: Back tapped")
- }) {
- Image(systemName: "arrow.left")
- .font(.headline)
- }
- },
- trailingButton: {
- Button(action: {
- print("Override: Save tapped")
- }) {
- Text("Save")
- .font(.headline)
- }
- }
- )
- .notchOverride(.series(prefix: "iPhone14", scale: 0.6, heightFactor: 0.6))
- .previewDisplayName("With View Override")
-}
-
-#Preview{
- // Another variant with different styling
- TopologyButtonsView(
- leadingButton: {
- Button(action: {
- print("Styled: Cancel tapped")
- }) {
- Text("Cancel")
- .font(.subheadline)
- .foregroundColor(.red)
- }
- },
- trailingButton: {
- Button(action: {
- print("Styled: Confirm tapped")
- }) {
- Text("Confirm")
- .font(.subheadline)
- .foregroundColor(.green)
- }
- }
- )
- .previewDisplayName("Custom Styled")
-}