Code Monkey home page Code Monkey logo

markbattistella / bezelkit Goto Github PK

View Code? Open in Web Editor NEW
16.0 2.0 2.0 598 KB

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.

License: MIT License

Swift 100.00%
corner-radius package-manager swift swift-package-manager swiftui ui-design hacktoberfest open-source ios macos

bezelkit's Introduction

Icon of Package

BezelKit

Perfecting Corners, One Radius at a Time

Languages Platforms Licence

Overview

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.

Rationale

Quick summary

  • 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

Longer explanation

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:

  1. If the actual bezel radius is smaller or larger than the static value, the UI corners will appear disproportionately thick or thin.

    Zoomed - Static Value

  2. 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:

Zoomed - BezelKit

Compatibility

Devices

In terms of the devices supported though, it covers from the initial versions of all devices. See the supported device list.

Installation

Swift Package Manager

The BezelKit package uses Swift Package Manager (SPM) for easy and convenient distribution. Follow these steps to add it to your project:

  1. In Xcode, click File -> Swift Packages -> Add Package Dependency

  2. In the search bar, type https://github.com/markbattistella/BezelKit and click Next.

  3. 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

  4. Finally, select the target in which you want to use BezelKit and click Finish.

Usage

Using BezelKit is simple and can help you avoid complexities related to device metrics.

Quick Start

  1. Import the BezelKit module:

    import BezelKit
  2. 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.

Perfect Scaling

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.

Perfect scaling

Setting a Fallback Bezel Size

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.

Enable Zero-Check Option

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.

UIKit: Setting the Fallback in AppDelegate

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
  }
}

SwiftUI: Setting the Fallback on Appear

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

Note

You only need to call setFallbackDeviceBezel(_:ifZero:) once. Doing this as early as possible ensures the fallback setting is applied throughout your application.

Effects of Setting a Fallback

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

Handling Errors with BezelKit

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.

SwiftUI: Error handling

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
          }
      }
    }
}

UIKit: Error handling

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
  }
}

Comparison

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()
  }
}

Comparison - Static Values

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()
  }
}

Comparison - BezelKit

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.

Things to note

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.

Generating New Bezels

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.

Contributing

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.

Licence

The BezelKit package is released under the MIT licence. See LICENCE for more information.

bezelkit's People

Contributors

markbattistella avatar markbattistella-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

bezelkit's Issues

Cleanup to the folder structure

This is a pending item where I want to clean up the folder structure.

Current local implementation has it listed as:

BezelKit
- README.md
- Package.swift
- LICENCE
- SupportedDeviceList.md
- Data
  - Images for README
- Generator
  - Javascript and worker Xcode project
- Sources
  - BezelKit library

This way the Sources contains only the required package contents, where the Generator will be purely for creating new data.

`index.js` script not working

There is some issue with the index.js script where it isn't correctly moving completed identifiers to the correct text file.

Workaround: manually add identifiers into the correct lists.

๐Ÿ› Fallback bezel size doesn't work on first run

Describe the bug

When setting a fallback size that's not zero with ifZero: true, the fallback size isn't applied when the app loads on iPhone SE / 7. As far as I can tell, it only shows the expected radius after updating it manually.

Reproduction steps

When I run the following code on an iPhone SE in simulator and on a physical iPhone 7, the corners show up completely sharp. The corners then update to the correct cornerRadius only after changing edgeSize. No errors or warnings are shown.

import SwiftUI

@main
struct BezelKit_TestApp: App {
	var body: some Scene {
		WindowGroup {
			ContentView()
				.onAppear() {
					// Sets a fallback value of 10.0 and enables zero-check
					CGFloat.setFallbackDeviceBezel(40.0, ifZero: true)
				}
		}
	}
}
import BezelKit
import SwiftUI

struct ContentView: View {
	@State private var edgeSize: CGFloat = 10
	
	@State private var showErrorAlert: Bool = false
	@State private var errorMessage: String = ""
	
	var body: some View {
		let outerBezel = CGFloat.deviceBezel
		let innerBezel = outerBezel - edgeSize
		
		VStack {
			Image(systemName: "globe")
				.imageScale(.large)
				.foregroundStyle(.tint)
			Text("Hello, world!")
			Slider(value: $edgeSize, in: 0...55)
			Text("\(edgeSize)")
		}
		.padding()
		.frame(maxWidth: .infinity, maxHeight: .infinity)
		.background(Color.yellow)
		.clipShape(RoundedRectangle(cornerRadius: innerBezel))
		.padding(edgeSize)
		.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
			}
		}
	}
}

Expected behaviour

View should have rounded corners when app first loads.

Screenshots

bezelkit

Device information

iPhone SE in simulator, physical iPhone 7 running v 15.7.9

Error in logs about Watch

There is an error in logs about Watch not found:

Failed to load device data: keyNotFound(CodingKeys(stringValue: "Watch", 
intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: 
"No value associated with key CodingKeys(stringValue: \"Watch\", intValue: nil) 
(\"Watch\").", underlyingError: nil))

@markbattistella (I) caused this when testing the Github Action for generating the SupportedDeviceList.md file on a CRON.

Modularise `index.js` file

Though this is a Swift Package, the backbone of the extraction is built on Javascript for the parsing and xcrun simctl commands.

The index.js file is very large and should be extracted into smaller, manageable files - just as we would with Swift code

โœจ Missing devices

Currently missing the following devices from the list:

    "iPad13,16": { "name": "iPad Air (5th generation)" },
    "iPad13,17": { "name": "iPad Air (5th generation)" },
    "iPad13,18": { "name": "iPad (10th generation)" },
    "iPad13,19": { "name": "iPad (10th generation)" },

    "iPad14,1": { "name": "iPad mini (6th generation)" },
    "iPad14,2": { "name": "iPad mini (6th generation)" },

    "iPad14,3": { "name": "iPad Pro (11-inch) (4th generation)" },
    "iPad14,4": { "name": "iPad Pro (11-inch) (4th generation)" },
    "iPad14,5": { "name": "iPad Pro (12.9-inch) (6th generation)" },
    "iPad14,6": { "name": "iPad Pro (12.9-inch) (6th generation)" },

    "iPad14,10": { "name": "iPad Air 13-inch (M2)" },
    "iPad14,11": { "name": "iPad Air 13-inch (M2)" },

    "iPad14,8": { "name": "iPad Air 11-inch (M2)" },
    "iPad14,9": { "name": "iPad Air 11-inch (M2)" },

    "iPad16,3": { "name": "iPad Pro 11-inch (M4)" },
    "iPad16,4": { "name": "iPad Pro 11-inch (M4)" },
    "iPad16,5": { "name": "iPad Pro 13-inch (M4)" },
    "iPad16,6": { "name": "iPad Pro 13-inch (M4)" },

    "iPad6,7": { "name": "iPad Pro (12.9-inch) (1st generation)" },
    "iPad6,8": { "name": "iPad Pro (12.9-inch) (1st generation)" },
    
    "iPad8,1": { "name": "iPad Pro (11-inch) (3rd generation)" },
    "iPad8,2": { "name": "iPad Pro (11-inch) (3rd generation)" },
    "iPad8,3": { "name": "iPad Pro (11-inch) (3rd generation)" },
    "iPad8,4": { "name": "iPad Pro (11-inch) (3rd generation)" },
    "iPad8,5": { "name": "iPad Pro (12.9-inch) (3rd generation)" },
    "iPad8,6": { "name": "iPad Pro (12.9-inch) (3rd generation)" },
    "iPad8,7": { "name": "iPad Pro (12.9-inch) (3rd generation)" },
    "iPad8,8": { "name": "iPad Pro (12.9-inch) (3rd generation)" },
    
    "iPad8,9": { "name": "iPad Pro (11-inch) (4th generation)" },
    "iPad8,10": { "name": "iPad Pro (11-inch) (4th generation)" },
    "iPad8,11": { "name": "iPad Pro (12.9-inch) (4th generation)" },
    "iPad8,12": { "name": "iPad Pro (12.9-inch) (4th generation)" }  

Extract "Generator" into own repo

Moving the Generator into its own repo might allow Javascript developers worry about that codebase without being worried about Swift code.

It also will hopefully slim down the package to only the Swift codebase

`setFallbackDeviceBezel()` not working on `0.0` values

When the device is reporting a value of 0.0 from the JSON the fallback doesnt override it.

It should be an optional flag where the user can override the 0.0 value to some sort of curved value.

Why this is good

Currently it's suggested we do this:

extension CGFloat {
  static let deviceRadius = Self.deviceBezel
 }

But in doing so, we will report a 0.0 on older device models.

But if we want to use it as a consistent radius then we could use something like this:

extension CGFloat {
  static let deviceRadius = deviceBezel == .zero ? fallbackValue : deviceBezel
}

But in some better fashion which allows the user to override 0 or not.

Remove watchOS

watchOS doesn't have any simple way to access the curvature or bezel radius.

To avoid confusion it should be removed unless there is a way to extract the sizes.

Re-running `index.js` results in 0.0 bezel data

When re-running simulator data simulators that were not processed results in 0.0 bezel size.

This is a result of merging the new bezel data into the CSV (parsed to JSON) variable, which defaults all values to 0.0.

Add iPhone 15 models

Add in new models:

  • iPhone 15,"iPhone15,4"
  • iPhone 15 Plus,"iPhone15,5"
  • iPhone 15 Pro,"iPhone16,1"
  • iPhone 15 Pro Max,"iPhone16,2"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.