Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Restructured `Sources` into a platform-isolated layout
(Core / Internal / Metrics / Scaling / UIKit / SwiftUI / Debug).
- SwiftUI `responsiveCornerRadius` now uses `clipShape` (deprecated `.cornerRadius` removed).
- Replaced all `print` logging with unified logging (`os.Logger`, subsystem `com.screenutil`).
- Raised deployment baseline to iOS 15 / macOS 12 / tvOS 15 / watchOS 8.
- `ScreenDimensions.current` now derives from the active `UIWindowScene` instead of the soft-deprecated `UIScreen.main`.
- Public value types (`ScalingLimits`, `ScreenMetrics`, `ScreenDimensions`, `SafeAreaInsets`, `ScreenUtilConfiguration`, `ScaleType`, `DeviceType`) now conform to `Equatable`/`Hashable`.

### Fixed
- Scale-factor data race on concurrent reads during reconfigure.
Expand All @@ -29,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ScaleType` cases `.font`, `.min`, `.max`, `.auto` (now `.width`, `.height`, `.text`, `.radius`).
- `ScreenUtilConfiguration.deviceType` — use `ScreenUtil.shared.deviceType`.
- Dead SwiftUI modifiers/wrappers, duplicate types, and the buggy `Array<Numeric>` scaling extension.
- `ScreenUtilConfiguration.splitScreenMode` — unused, removed from the config and `ScreenUtilConfigurable`.
- `ScaledValue.ScaleType` nested enum — `@ScaledValue` now uses the top-level `ScaleType` (`.font` → `.text`, `.auto` removed; default is `.width`).
- Benchmark APIs (`measurePerformance`, `benchmarkScalingOperations`) are now `#if DEBUG`-only.

### Dependencies
- Added [apple/swift-atomics](https://github.com/apple/swift-atomics) for the lock-free snapshot.
Expand Down
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import PackageDescription
let package = Package(
name: "ScreenUtil",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6)
.iOS(.v15),
.macOS(.v12),
.tvOS(.v15),
.watchOS(.v8)
],
products: [
.library(
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,12 @@ var batchScaler: BatchScaler

```swift
ScreenUtil.shared.debug.printCurrentConfiguration()
ScreenUtil.shared.debug.benchmarkScalingOperations()
_ = ScreenUtil.shared.debug.validateScaling()
let report = ScreenUtil.shared.debug.generateTestReport()

#if DEBUG
ScreenUtil.shared.debug.showDebugOverlay(on: view) // UIKit
ScreenUtil.shared.debug.benchmarkScalingOperations() // DEBUG builds only
ScreenUtil.shared.debug.showDebugOverlay(on: view) // UIKit
#endif
```

Expand All @@ -278,7 +278,7 @@ swift test --sanitize=thread # data-race check

## 📱 Device Support

iPhone, iPad (incl. Split View), plus macOS / tvOS / watchOS builds. iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+.
iPhone, iPad (incl. Split View), plus macOS / tvOS / watchOS builds. iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+.

## 🤝 Contributing

Expand Down
27 changes: 22 additions & 5 deletions Sources/ScreenUtil/Core/ScaleType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,46 @@
import Foundation
import CoreGraphics

public protocol ScreenUtilConfigurable {
/// Describes a type that holds the core ScreenUtil configuration values.
public protocol ScreenUtilConfigurable: Sendable {
/// The reference design canvas size (e.g. 375×812 from Figma).
var designSize: CGSize { get set }
/// When `true`, text scaling uses the minimum of the width and height factors to prevent oversized text.
var minTextAdapt: Bool { get set }
var splitScreenMode: Bool { get set }
/// The min/max bounds applied to all computed scale factors.
var scalingLimits: ScalingLimits { get set }
}

public protocol ScreenScalable {
/// Describes a type that can scale a value using a given `ScaleType`.
public protocol ScreenScalable: Sendable {
/// Returns the scaled value for `value` using the specified `scaleType`.
func scale(for value: CGFloat, scaleType: ScaleType) -> CGFloat
/// Returns a fast (cached-factor) scaled value for `value` using the specified `scaleType`.
func fastScale(for value: CGFloat, scaleType: ScaleType) -> CGFloat
}

public protocol ScreenDimensionProvider {
/// Describes a type that exposes current device screen dimensions and inset metrics.
public protocol ScreenDimensionProvider: Sendable {
/// The logical screen width in points.
var screenWidth: CGFloat { get }
/// The logical screen height in points.
var screenHeight: CGFloat { get }
/// The safe-area inset at the top of the screen in points.
var safeAreaTop: CGFloat { get }
/// The safe-area inset at the bottom of the screen in points.
var safeAreaBottom: CGFloat { get }
/// The status bar height in points.
var statusBarHeight: CGFloat { get }
}

public enum ScaleType: Sendable {
/// The dimension axis a value is scaled against.
public enum ScaleType: Sendable, Equatable, Hashable {
/// Scale by the width factor (design width → device width).
case width
/// Scale by the height factor (design height → device height).
case height
/// Scale by the text factor (min of width/height when `minTextAdapt` is enabled).
case text
/// Scale by the smaller of the width and height factors, suitable for corner radii.
case radius
}
10 changes: 9 additions & 1 deletion Sources/ScreenUtil/Core/ScalingLimits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,30 @@
import Foundation
import CoreGraphics

public struct ScalingLimits: Sendable {
/// Defines the minimum and maximum bounds that are clamped onto computed scale factors.
public struct ScalingLimits: Sendable, Equatable, Hashable {
/// The lower bound for any scale factor; clamped to a minimum of 0.1 at initialisation.
public let minScale: CGFloat
/// The upper bound for any scale factor; clamped to a maximum of 5.0 at initialisation.
public let maxScale: CGFloat

/// Creates a `ScalingLimits` with the given min/max bounds (clamped to the absolute safe range 0.1–5.0).
public init(minScale: CGFloat = 0.5, maxScale: CGFloat = 2.0) {
self.minScale = max(0.1, minScale)
self.maxScale = min(5.0, maxScale)
}

/// Returns `value` clamped to `[minScale, maxScale]`.
public func clamp(_ value: CGFloat) -> CGFloat {
return min(maxScale, max(minScale, value))
}

/// Default limits: 0.5–2.0, suitable for most iPhone designs.
public static let `default` = ScalingLimits()

/// Strict limits: 0.8–1.25, recommended for iPad and large-canvas designs.
public static let strict = ScalingLimits(minScale: 0.8, maxScale: 1.25)

/// Relaxed limits: 0.3–3.0, allowing wider scaling for experimental or TV layouts.
public static let relaxed = ScalingLimits(minScale: 0.3, maxScale: 3.0)
}
28 changes: 27 additions & 1 deletion Sources/ScreenUtil/Core/ScreenMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@
import Foundation
import CoreGraphics

public struct ScreenMetrics: Sendable {
/// A point-in-time snapshot of the device screen metrics used for scaling.
public struct ScreenMetrics: Sendable, Equatable, Hashable {
/// Logical screen width in points.
public let width: CGFloat
/// Logical screen height in points.
public let height: CGFloat
/// Native pixel density scale factor (e.g. 2.0 for @2x displays).
public let scale: CGFloat
/// Safe-area insets (top, bottom, left, right) in points.
public let safeAreaInsets: (top: CGFloat, bottom: CGFloat, left: CGFloat, right: CGFloat)
/// Status bar height in points.
public let statusBarHeight: CGFloat

/// Creates a `ScreenMetrics` snapshot with the given screen dimensions and insets.
public init(
width: CGFloat,
height: CGFloat,
Expand All @@ -29,4 +36,23 @@ public struct ScreenMetrics: Sendable {
self.safeAreaInsets = safeAreaInsets
self.statusBarHeight = statusBarHeight
}

public static func == (lhs: ScreenMetrics, rhs: ScreenMetrics) -> Bool {
lhs.width == rhs.width &&
lhs.height == rhs.height &&
lhs.scale == rhs.scale &&
lhs.safeAreaInsets == rhs.safeAreaInsets &&
lhs.statusBarHeight == rhs.statusBarHeight
}

public func hash(into hasher: inout Hasher) {
hasher.combine(width)
hasher.combine(height)
hasher.combine(scale)
hasher.combine(safeAreaInsets.top)
hasher.combine(safeAreaInsets.bottom)
hasher.combine(safeAreaInsets.left)
hasher.combine(safeAreaInsets.right)
hasher.combine(statusBarHeight)
}
}
4 changes: 1 addition & 3 deletions Sources/ScreenUtil/Core/ScreenUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,7 @@ public final class ScreenUtil: ScreenScalable, ScreenDimensionProvider, Sendable
@inline(__always)
public func scale(for value: CGFloat, scaleType: ScaleType) -> CGFloat {
guard value.isFinite else {
#if DEBUG
print("⚠️ ScreenUtil: Invalid input value \(value) for scale type \(scaleType)")
#endif
Log(.core, "Invalid input value \(value) for scale type \(scaleType)", level: .warning)
return 0
}
return value * scaleFactor(for: scaleType)
Expand Down
18 changes: 14 additions & 4 deletions Sources/ScreenUtil/Core/ScreenUtilConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,64 @@
import Foundation
import CoreGraphics

public struct ScreenUtilConfiguration: ScreenUtilConfigurable, Sendable {
/// Configuration for the ScreenUtil scaling engine, specifying the design canvas and scaling behaviour.
public struct ScreenUtilConfiguration: ScreenUtilConfigurable, Sendable, Equatable, Hashable {
/// The reference design canvas size used to compute scale factors.
public var designSize: CGSize
/// When `true`, font scaling uses the minimum of width/height factors to prevent text from becoming too large.
public var minTextAdapt: Bool
public var splitScreenMode: Bool
/// The min/max bounds clamped onto all computed scale factors.
public var scalingLimits: ScalingLimits

/// Creates a configuration with the given design size, text adaptation flag, and scaling limits.
public init(
designSize: CGSize = CGSize(width: 375, height: 812),
minTextAdapt: Bool = true,
splitScreenMode: Bool = true,
scalingLimits: ScalingLimits = .default
) {
self.designSize = designSize
self.minTextAdapt = minTextAdapt
self.splitScreenMode = splitScreenMode
self.scalingLimits = scalingLimits
}

/// Preset for designs targeting the iPhone 13 Pro (390×844 pt).
public static let iPhone13Pro = ScreenUtilConfiguration(
designSize: CGSize(width: 390, height: 844)
)

/// Preset for designs targeting the iPhone 14 Pro (393×852 pt).
public static let iPhone14Pro = ScreenUtilConfiguration(
designSize: CGSize(width: 393, height: 852)
)

/// Preset for designs targeting the iPhone 15 Pro (393×852 pt).
public static let iPhone15Pro = ScreenUtilConfiguration(
designSize: CGSize(width: 393, height: 852)
)

/// Preset for designs targeting the iPhone X / 11 Pro / 12 mini (375×812 pt).
public static let iPhoneX = ScreenUtilConfiguration(
designSize: CGSize(width: 375, height: 812)
)

/// Preset for designs targeting the iPhone 8 / SE 2nd gen (375×667 pt).
public static let iPhone8 = ScreenUtilConfiguration(
designSize: CGSize(width: 375, height: 667)
)

/// Preset for designs targeting the 11-inch iPad Pro (834×1194 pt) with strict scaling limits.
public static let iPadPro11 = ScreenUtilConfiguration(
designSize: CGSize(width: 834, height: 1194),
scalingLimits: .strict
)

/// Preset for designs targeting the 12.9-inch iPad Pro (1024×1366 pt) with strict scaling limits.
public static let iPadPro12_9 = ScreenUtilConfiguration(
designSize: CGSize(width: 1024, height: 1366),
scalingLimits: .strict
)

/// Returns `true` when `designSize` has positive, in-range dimensions (width ≤ 2000, height ≤ 3000).
public func isValidDesignSize() -> Bool {
return designSize.width > 0 && designSize.height > 0 &&
designSize.width <= 2000 && designSize.height <= 3000
Expand Down
Loading
Loading