BezelKit is a Swift package designed to simplify the process of accessing device-specific bezel sizes in apps.
Knowing the exact bezel size can be crucial for aligning UI elements, creating immersive experiences, or when you need pixel-perfect design layouts.
By providing an easy-to-use API, BezelKit
allows developers to focus more on their app's functionality rather than wrestling with device metrics.
- There is no public API from Apple for fetching device bezel sizes
- Using the internal API can jeopardise App Store eligibility
- Static bezel values can cause UI distortions across devices
BezelKit
offers an easy-to-use solution for accurate bezel metrics
Apple currently does not offer a public API for fetching the bezel radius of its devices.
Although an internal API exists, using it jeopardises the app's eligibility for the App Store — a risk that's not justifiable for a mere UI element.
Another consideration stems from the variability in screen bezel dimensions across different Apple devices. Setting a static bezel value is problematic for several reasons:
-
If the actual bezel radius is smaller or larger than the static value, the UI corners will appear disproportionately thick or thin.
-
On older devices or those with square screens, such as the SE models, the display will inaccurately feature curved corners when it should not.
While Apple has provided the ContainerRelativeShape
inset, its functionality is currently limited to Widgets. For all other applications, this API reports a squared rectangle, making it unsuitable for our needs.
A nice looking solution would look like this:
In terms of the devices supported though, it covers from the initial versions of all devices. See the supported device list.
The BezelKit package uses Swift Package Manager (SPM) for easy and convenient distribution. Follow these steps to add it to your project:
-
In Xcode, click
File -> Swift Packages -> Add Package Dependency
-
In the search bar, type
https://github.com/markbattistella/BezelKit
and clickNext
. -
Specify the version you want to use. You can select the exact version, use the latest one, or set a version range, and then click
Next
[!Tip] It's ideal to check the change log for differences across versions
-
Finally, select the target in which you want to use
BezelKit
and clickFinish
.
Using BezelKit
is simple and can help you avoid complexities related to device metrics.
-
Import the BezelKit module:
import BezelKit
-
Access the device bezel size:
let currentBezel = CGFloat.deviceBezel
For advanced usage, including perfect scaling of UI elements and setting fallback sizes, read the sections below.
The BezelKit
package not only provides an easy way to access device-specific bezel sizes but also enables perfect scaling of rounded corners within the UI.
When you have a rounded corner on the outer layer and an inner UI element that also needs a rounded corner, maintaining a perfect aspect ratio becomes essential for a harmonious design. This ensures your UI scales beautifully across different devices.
Here's how to implement it:
let outerBezel = CGFloat.BezelKit
let innerBezel = outerBezel - distance // Perfect ratio
By following this approach, you can ensure that your UI elements scale perfectly in relation to the device's bezel size.
You can use the deviceBezel(with:)
function to pass in the margin size, and it will return the device bezel but perfectly scaled with the inner ratio.
The package provides an easy way to specify a fallback bezel size. By default, the CGFloat.deviceBezel
attribute returns 0.0
if it cannot ascertain the device's bezel size.
In addition to specifying the fallback value, you have the option to return the fallback value even when the bezel size is determined to be zero.
For UIKit-based applications, you can set the fallback value in the AppDelegate
within the application(_:didFinishLaunchingWithOptions:)
method. This is the earliest point at which you can set the fallback value for your app.
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Sets a fallback value of 10.0 and enables zero-check
CGFloat.setFallbackDeviceBezel(10.0, ifZero: true)
return true
}
}
For SwiftUI applications, you can set this value in the init()
function for your main content view.
import SwiftUI
@main
struct YourApp: App {
init() {
// Sets a fallback value of 10.0 and enables zero-check
CGFloat.setFallbackDeviceBezel(10.0, ifZero: true)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Important
Previously it was noted to use the .onAppear
modifier for setting the fallback. This caused a bug of not updating on launch
You only need to call setFallbackDeviceBezel(_:ifZero:)
once. Doing this as early as possible ensures the fallback setting is applied throughout your application.
If you've set a fallback value, CGFloat.deviceBezel
will return this fallback value when it cannot determine the bezel size for the current device, or optionally, when the bezel size is zero.
// With fallback set to 10.0 and zero check enabled
let currentBezel = CGFloat.deviceBezel
print("Current device bezel: \(currentBezel)")
// Output will be 10.0 if the device is not in the JSON data or the bezel is zero
If no fallback is set, CGFloat.BezelKit
defaults to 0.0
when the device-specific bezel size is unavailable.
// With no fallback set and zero check disabled
let currentBezel = CGFloat.deviceBezel
print("Current device bezel: \(currentBezel)")
// Output will be 0.0 if the device is not in the JSON data
BezelKit offers optional error handling to manage unexpected issues like missing device bezel data or data parsing problems.
By using BezelKit's error callback, developers are alerted of these hiccups, allowing them to handle them as they see fit, whether it's logging for debugging or user notifications.
This ensures a smoother and more resilient app experience.
import SwiftUI
import BezelKit
struct ContentView: View {
@State private var showErrorAlert: Bool = false
@State private var errorMessage: String = ""
var body: some View {
RoundedRectangle(cornerRadius: .deviceBezel)
.stroke(Color.green, lineWidth: 20)
.ignoresSafeArea()
.alert(isPresented: $showErrorAlert) {
Alert(title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Got it!")))
}
.onAppear {
DeviceBezel.errorCallback = { error in
errorMessage = error.localizedDescription
showErrorAlert = true
}
}
}
}
import UIKit
import BezelKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DeviceBezel.errorCallback = { [weak self] error in
let alert = UIAlertController(title: "Error",
message: error.localizedDescription,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
let bezelValue = CGFloat.deviceBezel
// Use bezelValue for your views
}
}
This is a comparison between using a static, single value for all devices and how it looks when rendered compared to BezelKit
which will adapt to each device.
This was the code when using a static, single value for all devices:
import SwiftUI
struct ContentView: View {
var body: some View {
RoundedRectangle(cornerRadius: 60)
.stroke(.green, lineWidth: 20)
.ignoresSafeArea()
}
}
In a fixed value configuration, devices with no curved screen look odd, while this cornerRadius
is designed for the iPhone 14 Pro Max, it looks chunky on the iPhone 14, and good-ish on the iPhone 14 Pro.
This was the code when using a BezelKit
:
import SwiftUI
import BezelKit
struct ContentView: View {
var body: some View {
RoundedRectangle(cornerRadius: .deviceBezel)
.stroke(.green, lineWidth: 20)
.ignoresSafeArea()
}
}
As you can see, with no setFallbackBezelKit
set, the iPhone SE (3rd generation) value is set to 0.0
and results in no curve. However, all other curved devices have a consistent look.
BezelKit
does not currently support the affects from display zooming. When the generator runs, it performs all extractions on the "Standard" zoom level of the device.
If this was run on the "Zoomed" level, then the bezel radius would be different. However, since the physical device cannot change based on the zoom level, using "Standard" is the correct CGFloat number.
There is also no way to automate zoom levels in xcrun simctl
so it would have to be a manual inclusion, and at this point in time (unless raised via Issues) there is no really benefit for using the zoomed value for _displayRoundedCorner
.
For generating new bezels please refer to the BezelKit - Generator
repository.
When running the script it is best to do so from the BezelKit
directory as one of the script lines is to copy the compiled JSON into the /Resources
directory. This will not exist from the view of the generator repo.
Contributions are more than welcome. If you find a bug or have an idea for an enhancement, please open an issue or provide a pull request.
Please follow the code style present in the current code base when making contributions.
Note
Any pull requests need to have the title in the following format, otherwise it will be rejected.
YYYY-mm-dd - {title}
eg. 2023-08-24 - Updated README file
I like to track the day from logged request to completion, allowing sorting, and extraction of data. It helps me know how long things have been pending and provides OCD structure.
The BezelKit package is released under the MIT licence. See LICENCE for more information.