Skip to content

Commit

Permalink
2024-11-22: Added new deviceBezel function (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
markbattistella authored Nov 22, 2024
1 parent a0be25e commit 227b41b
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 167 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ By following this approach, you can ensure that your UI elements scale perfectly

![Perfect scaling](https://raw.githubusercontent.com/markbattistella/BezelKit/main/.github/data/ratio.jpg)

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.

### 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.
Expand Down
69 changes: 35 additions & 34 deletions Sources/BezelKit/BezelKit+Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,39 @@

import Foundation

internal extension DeviceBezel {

/// Converts `DecodingError` to the corresponding `DeviceBezelError`.
///
/// This function aids in interpreting and presenting more user-friendly error messages
/// based on the underlying decoding error.
///
/// - Parameter error: The `DecodingError` to be converted.
///
/// - Returns: The corresponding `DeviceBezelError` based on the provided `DecodingError`.
static func handleDecodingError(_ error: DecodingError) -> DeviceBezelError {
switch error {

case let .dataCorrupted(context):
// Indicates that data is corrupted or otherwise invalid for the associated type.
return .dataParsingFailed(context.debugDescription)

case let .keyNotFound(key, context):
// Indicates that a required key was not found in the data.
return .dataParsingFailed("Key '\(key)' not found: \(context.debugDescription)")

case let .valueNotFound(value, context):
// Indicates that a required value of a certain type was not found at the expected place in the data.
return .dataParsingFailed("Value '\(value)' not found: \(context.debugDescription)")

case let .typeMismatch(type, context):
// Indicates that encountered data was not of the expected type.
return .dataParsingFailed("Type '\(type)' mismatch: \(context.debugDescription)")

@unknown default:
// Catches any unknown or future error variants of DecodingError.
return .dataParsingFailed("Unknown DecodingError encountered.")
}
}
extension DeviceBezel {

/// Converts `DecodingError` to the corresponding `DeviceBezelError`.
///
/// This function aids in interpreting and presenting more user-friendly error messages
/// based on the underlying decoding error.
///
/// - Parameter error: The `DecodingError` to be converted.
///
/// - Returns: The corresponding `DeviceBezelError` based on the provided `DecodingError`.
static func handleDecodingError(_ error: DecodingError) -> DeviceBezelError {
switch error {

case let .dataCorrupted(context):
// Indicates that data is corrupted or otherwise invalid for the associated type.
return .dataParsingFailed(context.debugDescription)

case let .keyNotFound(key, context):
// Indicates that a required key was not found in the data.
return .dataParsingFailed("Key '\(key)' not found: \(context.debugDescription)")

case let .valueNotFound(value, context):
// Indicates that a required value of a certain type was not found at the expected
// place in the data.
return .dataParsingFailed("Value '\(value)' not found: \(context.debugDescription)")

case let .typeMismatch(type, context):
// Indicates that encountered data was not of the expected type.
return .dataParsingFailed("Type '\(type)' mismatch: \(context.debugDescription)")

@unknown default:
// Catches any unknown or future error variants of DecodingError.
return .dataParsingFailed("Unknown DecodingError encountered.")
}
}
}
143 changes: 74 additions & 69 deletions Sources/BezelKit/BezelKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,91 +10,96 @@ import UIKit
@MainActor
public class DeviceBezel {

/// An enumeration of errors that can occur when attempting to obtain a device's bezel thickness.
public enum DeviceBezelError: Error {
/// An enumeration of errors that can occur when attempting to obtain a device's bezel
/// thickness.
public enum DeviceBezelError: Error {

/// The resource needed to fetch bezel data could not be located.
case resourceNotFound
/// The resource needed to fetch bezel data could not be located.
case resourceNotFound

/// An error occurred when parsing the bezel data.
/// - Parameters:
/// - String: A description of the parsing error.
case dataParsingFailed(String)
}
/// An error occurred when parsing the bezel data.
/// - Parameters:
/// - String: A description of the parsing error.
case dataParsingFailed(String)
}

/// Cache to hold parsed device information.
/// Cache to hold parsed device information.
private static var devices: Devices?

/// A cache mapping device identifiers to their respective bezel thicknesses.
/// A cache mapping device identifiers to their respective bezel thicknesses.
private static var cache: [String: CGFloat] = [:]

/// A callback to be invoked when an error occurs during bezel data fetching or processing.
/// A callback to be invoked when an error occurs during bezel data fetching or processing.
public static var errorCallback: ((DeviceBezelError) -> Void)?

/// Loads device data from the JSON resource, decoding and caching the relevant information.
///
/// - Throws: An error of type `DeviceBezelError` if there's an issue accessing or decoding the data.
/// Loads device data from the JSON resource, decoding and caching the relevant information.
///
/// - Throws: An error of type `DeviceBezelError` if there's an issue accessing or decoding
/// the data.
private static func loadDeviceData() throws {
guard let url = Bundle.module.url(
forResource: "bezel",
withExtension: "min.json"
) else {
throw DeviceBezelError.resourceNotFound
}
guard
let url = Bundle.module.url(
forResource: "bezel",
withExtension: "min.json"
)
else {
throw DeviceBezelError.resourceNotFound
}

let data = try Data(contentsOf: url)
let decodedData = try JSONDecoder().decode(Database.self, from: data)
self.devices = decodedData.devices
cacheDevices(from: decodedData.devices)
}
let data = try Data(contentsOf: url)
let decodedData = try JSONDecoder().decode(Database.self, from: data)
self.devices = decodedData.devices
cacheDevices(from: decodedData.devices)
}

/// Caches bezel radius data for various device types: iPad, iPhone, and iPod.
///
/// - Parameters:
/// - devices: The `Devices` struct containing bezel information for different device types.
private static func cacheDevices(from devices: Devices) {
cacheDeviceType(devices.iPad)
cacheDeviceType(devices.iPhone)
cacheDeviceType(devices.iPod)
}
/// Caches bezel radius data for various device types: iPad, iPhone, and iPod.
///
/// - Parameters:
/// - devices: The `Devices` struct containing bezel information for different device types.
private static func cacheDevices(from devices: Devices) {
cacheDeviceType(devices.iPad)
cacheDeviceType(devices.iPhone)
cacheDeviceType(devices.iPod)
}

/// Helper function that populates the cache with bezel radius for a specific device type.
///
/// - Parameters:
/// - deviceType: A dictionary mapping device identifiers to their respective `DeviceInfo`.
private static func cacheDeviceType(_ deviceType: [String: DeviceInfo]) {
for (identifier, deviceInfo) in deviceType {
cache[identifier] = CGFloat(deviceInfo.bezel)
}
}
/// Helper function that populates the cache with bezel radius for a specific device type.
///
/// - Parameters:
/// - deviceType: A dictionary mapping device identifiers to their respective `DeviceInfo`.
private static func cacheDeviceType(_ deviceType: [String: DeviceInfo]) {
for (identifier, deviceInfo) in deviceType {
cache[identifier] = CGFloat(deviceInfo.bezel)
}
}
}

extension DeviceBezel {

/// Provides the bezel thickness for the current device.
///
/// If the data hasn't been loaded yet, this property will attempt to load it. If any errors occur during the loading or
/// processing, the registered error callback (if any) will be invoked.
///
/// - Returns: An optional `CGFloat` representing the bezel thickness for the current device.
/// Returns `nil` if the information isn't available or an error occurred.
public static var currentBezel: CGFloat? {
if devices == nil {
do {
try loadDeviceData()
} catch let error as DeviceBezelError {
errorCallback?(error)
return nil
} catch let error as DecodingError {
errorCallback?(handleDecodingError(error))
return nil
} catch {
errorCallback?(.dataParsingFailed(error.localizedDescription))
return nil
}
}
/// Provides the bezel thickness for the current device.
///
/// If the data hasn't been loaded yet, this property will attempt to load it. If any errors
/// occur during the loading or processing, the registered error callback (if any) will be
/// invoked.
///
/// - Returns: An optional `CGFloat` representing the bezel thickness for the current device.
/// Returns `nil` if the information isn't available or an error occurred.
public static var currentBezel: CGFloat? {
if devices == nil {
do {
try loadDeviceData()
} catch let error as DeviceBezelError {
errorCallback?(error)
return nil
} catch let error as DecodingError {
errorCallback?(handleDecodingError(error))
return nil
} catch {
errorCallback?(.dataParsingFailed(error.localizedDescription))
return nil
}
}

let identifier = UIDevice.current.modelIdentifier
return cache[identifier]
}
let identifier = UIDevice.current.modelIdentifier
return cache[identifier]
}
}
90 changes: 56 additions & 34 deletions Sources/BezelKit/CGFloat+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,60 @@
import Foundation

@MainActor
public extension CGFloat {

/// A fallback value used when the device bezel radius cannot be determined.
private static var fallbackBezelValue: CGFloat = 0.0

/// Indicates if the fallback value should be used when the determined bezel radius is zero.
private static var shouldFallbackIfZero: Bool = false

/// The bezel radius for the current device.
///
/// If the bezel radius is not available or an error occurs, this property will use the fallback value.
/// If the bezel radius is zero and `shouldFallbackIfZero` is set to true, it will also use the fallback value.
///
/// - Returns: A `CGFloat` representing the bezel radius for the current device or the fallback value if necessary.
static var deviceBezel: CGFloat {
if let currentBezel = DeviceBezel.currentBezel,
!(shouldFallbackIfZero &&
(currentBezel == 0.0 ||
currentBezel == 0 ||
currentBezel == .zero)) {
return currentBezel
}
return fallbackBezelValue
}

/// Sets a fallback value for the device bezel radius, to be used when the actual value cannot be determined.
///
/// - Parameters:
/// - value: The fallback bezel radius.
/// - zero: A Boolean indicating if the fallback value should be used when the determined bezel radius is zero.
static func setFallbackDeviceBezel(_ value: CGFloat, ifZero zero: Bool = false) {
fallbackBezelValue = value
shouldFallbackIfZero = zero
}
extension CGFloat {

/// A fallback value used when the device bezel radius cannot be determined.
private static var fallbackBezelValue: CGFloat = 0.0

/// Indicates if the fallback value should be used when the determined bezel radius is zero.
private static var shouldFallbackIfZero: Bool = false

/// The bezel radius for the current device.
///
/// If the bezel radius is not available or an error occurs, this property will use the
/// fallback value. If the bezel radius is zero and `shouldFallbackIfZero` is set to true,
/// it will also use the fallback value.
///
/// - Returns: A `CGFloat` representing the bezel radius for the current device or the
/// fallback value if necessary.
public static var deviceBezel: CGFloat {
if let currentBezel = DeviceBezel.currentBezel,
!(shouldFallbackIfZero
&& (currentBezel == 0.0 || currentBezel == 0 || currentBezel == .zero))
{
return currentBezel
}
return fallbackBezelValue
}

/// Calculates the bezel radius with a specified margin.
///
/// - Parameter margin: The margin to subtract from the bezel radius.
/// - Returns: The bezel radius adjusted by the margin.
public static func deviceBezel(with margin: CGFloat) -> CGFloat {
deviceBezel.innerRadius(with: margin)
}

/// Sets a fallback value for the device bezel radius, to be used when the actual value
/// cannot be determined.
///
/// - Parameters:
/// - value: The fallback bezel radius.
/// - zero: A Boolean indicating if the fallback value should be used when the determined
/// bezel radius is zero.
public static func setFallbackDeviceBezel(_ value: CGFloat, ifZero zero: Bool = false) {
fallbackBezelValue = value
shouldFallbackIfZero = zero
}
}

extension Numeric where Self: Comparable {

/// Calculates the inner radius by subtracting a given margin.
///
/// - Parameter margin: The margin to subtract from the current radius.
/// - Returns: The calculated inner radius.
func innerRadius(with margin: Self) -> Self {
return self - margin
}
}
Loading

0 comments on commit 227b41b

Please sign in to comment.