diff --git a/CHANGELOG.md b/CHANGELOG.md index cd927fd..f39f516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,49 +38,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. +- One dependency: [apple/swift-atomics](https://github.com/apple/swift-atomics), used for the lock-free scale-factor snapshot. -## [1.0.0] - 2025-01-XX - -### Added -- **Core ScreenUtil class** with singleton pattern for responsive screen adaptation -- **Screen dimension properties**: `screenWidth`, `screenHeight`, `scaleWidth`, `scaleHeight`, `scaleText` -- **Safe area support**: `topSafeArea`, `bottomSafeArea`, `statusBarHeight` -- **Device information**: `pixelRatio` for device pixel density -- **Configuration methods**: - - `setDesignSize(width:height:)` - Set reference design dimensions - - `setMinTextAdapt(_:)` - Enable/disable minimum text adaptation - - `setSplitScreenMode(_:)` - Enable/disable split screen support - - `setFontResolver(_:)` - Custom font scaling resolver - - `refresh()` - Manual screen dimension refresh -- **CGFloat extensions** for easy scaling: - - `.scaleWidth` - Scale by width factor - - `.scaleHeight` - Scale by height factor - - `.scaleText` - Scale for text (minimum factor) - - `.scale(by:)` - Scale by custom factor -- **SwiftUI support** with view modifiers and extensions -- **Orientation handling** with automatic updates -- **iOS 12.0+ and tvOS 12.0+ support** -- **Swift 6.1+ compatibility** -- **Comprehensive unit tests** for all core functionality -- **Apache 2.0 license** for open source distribution -- **Complete documentation** with README and API reference - -### Technical Details -- **Platform Support**: iOS 12.0+, tvOS 12.0+ -- **Swift Version**: 6.1+ -- **Xcode Version**: 15.0+ -- **Architecture**: Singleton pattern with thread-safe implementation -- **Performance**: Optimized for minimal overhead and efficient scaling calculations - -### Documentation -- Complete README.md with installation and usage examples -- API reference with all public methods and properties -- Code examples for both UIKit and SwiftUI integration -- Contributing guidelines and license information - ---- - -## Version History - -- **1.0.0** - Initial release with core responsive design functionality \ No newline at end of file +### Notes +- Pre-1.0: no version has been tagged yet, and the public API is still changing. + The current state is the platform-isolated Swift 6 engine described above; the + next tagged release will become `1.0.0`. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d02390b..1aad2a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # ScreenUtil -Responsive screen-adaptation library for Apple platforms. Scales UI from a fixed design size (e.g. 375×812 from Figma) to the real device. Inspired by `flutter_screenutil`. Pure Swift, zero dependencies. Builds on iOS / macOS / tvOS / watchOS. +Responsive screen-adaptation library for Apple platforms. Scales UI from a fixed design size (e.g. 375×812 from Figma) to the real device. Inspired by `flutter_screenutil`. Pure Swift, one dependency ([apple/swift-atomics](https://github.com/apple/swift-atomics), for the lock-free scale-factor snapshot). Builds on iOS 15 / macOS 12 / tvOS 15 / watchOS 8. This file orients code-review and cleanup work: it maps the code, marks the public contract that must not break, and records the rules that keep the package clean. @@ -35,6 +35,7 @@ Organizing rule: **one platform = one place.** Everything outside `UIKit/` and ` Anything not reachable from this set is a removal/merge candidate. - `ScreenUtil.shared` + `configure(with:)` — singleton, configured once at launch. +- `refreshMetrics()` — `@MainActor` rebuild of the snapshot (manual refresh, e.g. macOS window resize). - Numeric scaling: `.w .h .sp .r .sw .sh`. - `ScreenUtilConfiguration` (+ presets) and `ScalingLimits`. - `FastScale` / `withFastScale`, `BatchScaler` / `withBatchScaler`. @@ -43,7 +44,7 @@ Anything not reachable from this set is a removal/merge candidate. ## Review Checklist (when editing) -- Keep **zero dependencies** — `Package.swift` `dependencies: []` stays empty. +- Keep **exactly one dependency** — only `apple/swift-atomics` (for the atomic `Snapshot`). Don't add others; don't remove it (it's what makes reads lock-free and `ScreenUtil` `Sendable` without `@unchecked`). - **One platform = one place**: UIKit code only under `UIKit/` (or `#if canImport(UIKit)` blocks); SwiftUI only under `SwiftUI/`. Never bare `import UIKit` in cross-platform files — breaks macOS. - Builds under `-strict-concurrency=complete` — no new concurrency warnings. - `ScreenUtil` is compiler-verified `Sendable` (atomic `Snapshot`; no `@unchecked`). New shared state must be `Sendable` / atomic. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f57172..3bda4c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,9 +58,9 @@ We welcome pull requests! Please follow these guidelines: ### Prerequisites -- Xcode 15.0+ -- iOS 12.0+ / tvOS 12.0+ simulator or device -- Swift 6.1+ +- Xcode 16.0+ +- Swift 6.0 toolchain (the package builds in Swift 6 language mode) +- A simulator or device on iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+ ### Getting Started @@ -82,31 +82,37 @@ We welcome pull requests! Please follow these guidelines: ### Project Structure +Platform-isolated layout — anything outside `UIKit/` and `SwiftUI/` is pure +cross-platform (Foundation + CoreGraphics only), which keeps the macOS build safe. + ``` ScreenUtil/ -├── Package.swift # Package configuration -├── Sources/ -│ └── ScreenUtil/ -│ ├── ScreenUtil.swift # Main ScreenUtil class -│ ├── Extensions/ # CGFloat extensions -│ └── SwiftUI/ # SwiftUI support -└── Tests/ - └── ScreenUtilTests/ # Unit tests +├── Package.swift # Package configuration (one dependency: swift-atomics) +├── Sources/ScreenUtil/ +│ ├── Core/ # ScreenUtil engine, configuration, ScaleType, limits, metrics +│ ├── Internal/ # Atomic Snapshot, scale-factor cache, logging +│ ├── Metrics/ # ScreenDimensions, DeviceType +│ ├── Scaling/ # Numeric/CGGeometry extensions, FastScale, BatchScaler +│ ├── UIKit/ # UIFont / UIView / UIEdgeInsets helpers (#if canImport(UIKit)) +│ ├── SwiftUI/ # EnvironmentValues.screenUtil +│ └── Debug/ # ScreenUtilDebug +├── Tests/ScreenUtilTests/ # Unit tests (Core / Scaling / SwiftUI / Debug / Performance) +└── Examples/ # Runnable SwiftUI + UIKit demo apps (XcodeGen) ``` ## Coding Standards ### Swift Style Guide -We follow the [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) and use SwiftLint for consistency. +We follow the [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/). ### Code Formatting - Use **4 spaces** for indentation (no tabs) - Maximum line length: **120 characters** - Use **camelCase** for variables and functions -- Use **PascalCase** for types and protocols -- Use **snake_case** for file names +- Use **PascalCase** for types, protocols, **and file names** (e.g. `ScreenUtil.swift`, `ScaleType.swift`) +- Each file starts with an Xcode-style header comment ending `Created by Dicky Darmawan on DD/MM/YY` ### Documentation @@ -192,9 +198,9 @@ final class ScreenUtilTests: XCTestCase { ### Before Submitting -1. **Ensure tests pass** locally +1. **Ensure tests pass** locally (`swift test`, and `swift test --sanitize=thread` for concurrency changes) 2. **Update documentation** if needed -3. **Check code formatting** with SwiftLint +3. **Check code formatting** against the style guide above 4. **Self-review** your changes ### Pull Request Template diff --git a/Examples/image/SwiftUI - iPhone 17 Pro Max.png b/Examples/image/SwiftUI - iPhone 17 Pro Max.png new file mode 100644 index 0000000..ebf7fbd Binary files /dev/null and b/Examples/image/SwiftUI - iPhone 17 Pro Max.png differ diff --git a/Examples/image/UIKit - iPhone 12.png b/Examples/image/UIKit - iPhone 12.png new file mode 100644 index 0000000..63e6999 Binary files /dev/null and b/Examples/image/UIKit - iPhone 12.png differ diff --git a/PLATFORM_SUPPORT.md b/PLATFORM_SUPPORT.md index 485ad9e..6f45728 100644 --- a/PLATFORM_SUPPORT.md +++ b/PLATFORM_SUPPORT.md @@ -2,196 +2,110 @@ ## Overview -ScreenUtil is designed to work across all Apple platforms without requiring UIKit as a dependency. The library automatically adapts to the available frameworks and provides fallback implementations where needed. +ScreenUtil runs on all Apple platforms. UIKit is **not** required: where it's +available the library reads real screen metrics and safe areas and auto-refreshes +on scene/orientation changes; where it isn't (macOS/watchOS, command-line, SwiftUI +previews) it falls back to platform-appropriate defaults that you can override via +`configure(with:)`. -## Platform Support +## Supported Platforms -### ✅ **iOS 13.0+** -- **UIKit Available**: Full functionality with automatic screen detection, safe area handling, and orientation change notifications -- **UIKit Unavailable**: Fallback to sensible defaults with manual configuration support +| Platform | Minimum | Screen metrics source | +|----------|---------|-----------------------| +| iOS / iPadOS | **15.0** | Active `UIWindowScene` (UIKit) | +| macOS | **12.0** | Platform default (no UIKit) | +| tvOS | **15.0** | UIKit | +| watchOS | **8.0** | Platform default | -### ✅ **macOS 10.15+** -- **AppKit/SwiftUI**: Core scaling functionality with platform-appropriate defaults -- **Default Screen**: 1440x900 @2x (MacBook Air equivalent) +## Dependencies -### ✅ **tvOS 13.0+** -- **UIKit Available**: Full TV-optimized functionality -- **Default Screen**: 1920x1080 @1x with appropriate safe areas for TV bezels - -### ✅ **watchOS 6.0+** -- **WatchKit**: Optimized for small screen scaling -- **Default Screen**: 184x224 @2x (Apple Watch Series 7 41mm equivalent) - -## Architecture - -### Core Dependencies ```swift -// Only these frameworks are required across all platforms: +// Required across all platforms: import Foundation import CoreGraphics -import QuartzCore // For CACurrentMediaTime -``` +import Atomics // apple/swift-atomics — the lock-free scale-factor snapshot -### Optional Dependencies -```swift -// These are conditionally imported only when available: +// Conditionally compiled, only where available: #if canImport(UIKit) -import UIKit // iOS, tvOS - for screen detection and safe areas +import UIKit // iOS, tvOS — screen detection, safe areas, change notifications #endif #if canImport(SwiftUI) -import SwiftUI // All platforms - for SwiftUI extensions -#endif - -#if canImport(AppKit) -import AppKit // macOS - for future AppKit-specific features +import SwiftUI // EnvironmentValues.screenUtil #endif ``` -### Fallback Strategy +There is exactly one external dependency, [apple/swift-atomics](https://github.com/apple/swift-atomics). + +## Fallback Strategy -1. **Screen Detection** - - **With UIKit**: Uses `UIScreen.main.bounds` and `UIDevice.current.userInterfaceIdiom` - - **Without UIKit**: Uses platform-appropriate defaults +1. **Screen detection** + - **With UIKit**: derived from the active `UIWindowScene` (not the soft-deprecated `UIScreen.main`). + - **Without UIKit**: platform default (see below). -2. **Safe Area Detection** - - **With UIKit**: Uses `UIApplication.shared.connectedScenes` and window safe areas - - **Without UIKit**: Uses platform-specific safe area defaults +2. **Safe-area detection** + - **With UIKit**: native safe-area insets captured from the window at rebuild time. + - **Without UIKit / before a window exists**: all insets are `0`. -3. **Orientation Changes** - - **With UIKit**: Automatically invalidates caches on `UIDevice.orientationDidChangeNotification` - - **Without UIKit**: Manual cache invalidation via `refreshMetrics()` +3. **Change handling** + - **With UIKit**: the snapshot rebuilds automatically on `UIScene.didActivate` and `UIDevice.orientationDidChange`. + - **Without UIKit**: call `ScreenUtil.shared.refreshMetrics()` manually (e.g. after a macOS window resize). ## Platform Defaults -### iOS (without UIKit) -```swift -// Screen: iPhone 13 Pro equivalent -ScreenDimensions(width: 375, height: 812, scale: 3.0) -SafeAreaInsets(top: 44, bottom: 34, left: 0, right: 0, statusBarHeight: 44) -``` +Used before configuration, and on platforms without UIKit. Safe-area insets and +status-bar height default to `0` until a real window provides them. -### macOS ```swift -// Screen: MacBook Air equivalent -ScreenDimensions(width: 1440, height: 900, scale: 2.0) -SafeAreaInsets(top: 0, bottom: 0, left: 0, right: 0, statusBarHeight: 24) +// iOS — ScreenDimensions(width: 375, height: 812, scale: 3.0) +// macOS — ScreenDimensions(width: 1440, height: 900, scale: 2.0) +// tvOS — ScreenDimensions(width: 1920, height: 1080, scale: 1.0) +// watchOS — ScreenDimensions(width: 184, height: 224, scale: 2.0) ``` -### tvOS (without UIKit) -```swift -// Screen: Apple TV 4K -ScreenDimensions(width: 1920, height: 1080, scale: 1.0) -SafeAreaInsets(top: 60, bottom: 60, left: 90, right: 90, statusBarHeight: 0) -``` +## Usage -### watchOS -```swift -// Screen: Apple Watch Series 7 41mm -ScreenDimensions(width: 184, height: 224, scale: 2.0) -SafeAreaInsets(top: 0, bottom: 0, left: 0, right: 0, statusBarHeight: 0) -``` +### Core scaling — all platforms, no UIKit needed -## Usage Examples - -### Core Scaling (All Platforms) ```swift -// These work on all platforms without any dependencies -let scaledWidth = 100.w -let scaledHeight = 50.h -let scaledFont = 16.sp -let scaledRadius = 12.r - -// Screen percentages -let halfScreen = 50.sw -let quarterScreen = 25.sh +let width = 100.w // width scaling +let height = 50.h // height scaling +let font = 16.sp // text scaling (no distortion) +let radius = 12.r // radius scaling + +let halfWidth = 50.sw // 50% of screen width +let tenthHeight = 10.sh // 10% of screen height ``` -### Platform-Independent Types -```swift -// ResponsiveInsets works on all platforms -let insets = ResponsiveInsets.responsive( - top: 16, leading: 20, bottom: 16, trailing: 20 -) +`.w/.h/.sp/.r/.sw/.sh` are available on `Int`, `Float`, `Double`, and `CGFloat`, +and `CGSize/CGPoint/CGRect` have their own scaling helpers — all cross-platform. -// Convert to platform-specific types when available -#if canImport(UIKit) -let uiInsets = insets.uiEdgeInsets -#endif +### UIKit-only helpers -#if canImport(SwiftUI) -let swiftUIInsets = insets.edgeInsets -#endif -``` +Wrap UIKit-specific code in `#if canImport(UIKit)` for cross-platform sources: -### Font Scaling ```swift -// Platform-independent font descriptor -let fontDesc = ResponsiveFontDescriptor(size: 16, weight: .medium) - #if canImport(UIKit) -let uiFont = fontDesc.uiFont -#endif - -#if canImport(SwiftUI) -let swiftUIFont = fontDesc.swiftUIFont +let font = UIFont.systemFont(ofSize: 16, weight: .medium, scaled: true) +let insets = UIEdgeInsets.scaled(horizontal: 20, vertical: 10) +view.cornerRadius(12) #endif ``` -### Manual Configuration for Non-UIKit Environments +### Manual configuration for non-UIKit environments + ```swift -// Explicitly set screen dimensions if UIKit detection isn't available -let config = ScreenUtilConfiguration( - designSize: CGSize(width: 375, height: 812) -) +// Set your design canvas explicitly (or use a preset like .iPhone12). +let config = ScreenUtilConfiguration(designSize: CGSize(width: 375, height: 812)) ScreenUtil.shared.configure(with: config) -// Manually refresh metrics when screen changes (e.g., window resize on macOS) +// Refresh after a screen change UIKit can't observe for you (e.g. macOS resize). ScreenUtil.shared.refreshMetrics() ``` ## Best Practices -1. **Always use the core scaling APIs** (`.w`, `.h`, `.sp`, `.r`) - they work everywhere -2. **Use platform-independent types** (`ResponsiveInsets`, `ResponsiveFontDescriptor`) for cross-platform code -3. **Wrap platform-specific code** in `#if canImport()` blocks -4. **Test on all target platforms** to ensure fallbacks work correctly -5. **Consider manual configuration** for specialized environments - -## Migration from UIKit-dependent Code - -### Before (UIKit Required) -```swift -let insets = UIEdgeInsets(top: 16.h, left: 20.w, bottom: 16.h, right: 20.w) -let font = UIFont.systemFont(ofSize: 16.sp, weight: .medium) -``` - -### After (Platform Independent) -```swift -let insets = ResponsiveInsets.responsive(top: 16, leading: 20, bottom: 16, trailing: 20) -let fontDesc = ResponsiveFontDescriptor(size: 16, weight: .medium) - -// Convert to platform types when needed -#if canImport(UIKit) -let uiInsets = insets.uiEdgeInsets -let uiFont = fontDesc.uiFont -#endif -``` - -## Performance Impact - -The optional UIKit approach has minimal performance impact: - -- **Compile-time**: Conditional compilation eliminates unused code -- **Runtime**: No performance difference when UIKit is available -- **Fallback mode**: Slightly faster due to no framework overhead -- **Memory**: Reduced memory usage when UIKit isn't needed - -This architecture ensures ScreenUtil can be used in: -- iOS apps (with or without UIKit) -- macOS apps (AppKit, SwiftUI, or Catalyst) -- tvOS apps -- watchOS apps -- Command-line tools -- Server-side Swift -- Cross-platform frameworks \ No newline at end of file +1. **Use the core scaling APIs** (`.w`, `.h`, `.sp`, `.r`) — they work everywhere. +2. **Wrap platform-specific code** in `#if canImport(UIKit)` / `#if canImport(SwiftUI)`. +3. **Configure once at launch** on the main actor; reads afterwards are lock-free. +4. **Test on all target platforms** (`swift build` + `swift test` on macOS catch the platform-isolation regressions). diff --git a/README.md b/README.md index 0cd5b05..0f204fb 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@

- Swift Version - Platforms + Swift Version + Platforms SPM Compatible - License + License

@@ -26,6 +26,24 @@ - 🖥️ **Multi-platform** — iOS, macOS, tvOS, watchOS > **Dependencies:** one — [apple/swift-atomics](https://github.com/apple/swift-atomics), used for the lock-free scale-factor snapshot. +> +> **Privacy:** ships a privacy manifest (`PrivacyInfo.xcprivacy`) declaring no data collection and no tracking. + +## 📲 Showcase + +Two runnable demo apps live in [`Examples/`](Examples) — one pure SwiftUI, one pure UIKit — each a GitHub profile page built on the **iPhone 12 baseline (390×844)** with an MVVM `LoadState` architecture. The same design scales proportionally across devices, staying visually identical: + +| UIKit · iPhone 12 (baseline 1:1) | SwiftUI · iPhone 17 Pro Max (scaled up) | +| :---: | :---: | +| UIKit demo on iPhone 12 | SwiftUI demo on iPhone 17 Pro Max | + +Generate the Xcode project with [XcodeGen](https://github.com/yonaskolb/XcodeGen): + +```bash +cd Examples +xcodegen generate +open ScreenUtilExamples.xcodeproj # schemes: "SwiftUI Demo", "UIKit Demo" +``` ## 📦 Installation @@ -286,7 +304,7 @@ See the [Contributing Guide](CONTRIBUTING.md). Fork → branch → PR. ## 📄 License -ScreenUtil is available under the MIT license. See [LICENSE](LICENSE). +ScreenUtil is available under the Apache 2.0 license. See [LICENSE](LICENSE). ## 🙏 Acknowledgments