diff --git a/.gitignore b/.gitignore index c94febd..aa7a394 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,10 @@ Examples/.build # Local-only project notes / planning artifacts (not committed) docs/ .remember/ +.superpowers/ # SPM -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata \ No newline at end of file +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata + +# Graphify +graphify-out/ diff --git a/CLAUDE.md b/CLAUDE.md index 841619f..f618e55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Organizing rule: **one platform = one place.** Everything outside `UIKit/` and ` | Path | Role | |------|------| -| `Core/ScreenUtil.swift` | Singleton engine (`ScreenUtil.shared`), `configure`, `scale`/`fastScale`, `.w/.h/.sp/.r/.sw/.sh/.fast*` | +| `Core/ScreenUtil.swift` | Singleton engine (`ScreenUtil.shared`), `configure`, `scale`/`fastScale`, `.w/.h/.sp/.r/.sw/.sh` | | `Core/ScreenUtilConfiguration.swift` | Config struct + device presets (`iPhone13Pro`, `iPadPro11`, …) | | `Core/ScaleType.swift` | Protocols (`ScreenScalable`, `ScreenUtilConfigurable`, `ScreenDimensionProvider`) + `ScaleType` | | `Core/ScalingLimits.swift` | `ScalingLimits` (`.default`/`.strict`/`.relaxed`) | @@ -22,26 +22,25 @@ Organizing rule: **one platform = one place.** Everything outside `UIKit/` and ` | `Metrics/ScreenDimensions.swift` | Platform dimensions snapshot (UIKit-gated reader) | | `Metrics/SafeAreaInsets.swift` | Platform safe-area snapshot (UIKit-gated reader) | | `Metrics/DeviceType.swift` | Device/platform classification | -| `Scaling/Numeric+Scaling.swift` | `Int/Float/Double/CGFloat` `.w/.h/.sp/.r/.sw/.sh/.fast*` | +| `Scaling/Numeric+Scaling.swift` | `Int/Float/Double/CGFloat` `.w/.h/.sp/.r/.sw/.sh` | | `Scaling/CGGeometry+Scaling.swift` | `CGSize/CGPoint/CGRect` scaling (cross-platform) | | `Scaling/FastScale.swift` | `FastScale` capture-once struct for hot loops + `withFastScale` | -| `Scaling/BatchScaling.swift` | `batch*` methods + `BatchScaler` for bulk scaling | +| `Scaling/BatchScaling.swift` | `BatchScaler` / `withBatchScaler` for bulk scaling | | `UIKit/UIFont+Scaling.swift` | `UIFont` scaled helpers (`#if canImport(UIKit)`) | | `UIKit/UIView+Scaling.swift` | `UIView` constraint/styling + `NSLayoutConstraint.updateConstant` | | `UIKit/UIEdgeInsets+Scaling.swift` | `UIEdgeInsets.scaled(...)` | -| `SwiftUI/View+Responsive.swift` | `View` modifiers, `Font.scaledSystem/scaledCustom`, `EnvironmentValues.screenUtil` | -| `SwiftUI/ScaledValue.swift` | `@ScaledValue`, `@ScreenPercentage` property wrappers | -| `Debug/ScreenUtilDebug.swift` | Debug logging, benchmarking, validation, overlay | +| `SwiftUI/Environment+ScreenUtil.swift` | `EnvironmentValues.screenUtil` (injects `.shared`) | +| `Debug/ScreenUtilDebug.swift` | `enum` namespace: debug logging, benchmarking, overlay | ## Public Contract (must not break) Anything not reachable from this set is a removal/merge candidate. - `ScreenUtil.shared` + `configure(with:)` — singleton, configured once at launch. -- Numeric scaling: `.w .h .sp .r .sw .sh` and fast variants `.fastW .fastH .fastSp`. +- Numeric scaling: `.w .h .sp .r .sw .sh`. - `ScreenUtilConfiguration` (+ presets) and `ScalingLimits`. -- `FastScale` / `withFastScale`, `BatchScaler` / `withBatchScaler` / `batch*`. -- SwiftUI: `responsiveFrame/responsivePadding/responsiveCornerRadius`, `Font.scaledSystem`, `@ScaledValue`, `@ScreenPercentage`. +- `FastScale` / `withFastScale`, `BatchScaler` / `withBatchScaler`. +- SwiftUI: `EnvironmentValues.screenUtil`. - UIKit: `UIFont.systemFont(…, scaled:)`, `UIView` helpers, `UIEdgeInsets.scaled`. ## Review Checklist (when editing) @@ -49,18 +48,11 @@ Anything not reachable from this set is a removal/merge candidate. - Keep **zero dependencies** — `Package.swift` `dependencies: []` stays empty. - **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 `@unchecked Sendable`. New shared state must be `Sendable` / atomic. +- `ScreenUtil` is compiler-verified `Sendable` (atomic `Snapshot`; no `@unchecked`). New shared state must be `Sendable` / atomic. - Bulk APIs accept `[T: Numeric]` — route through `cgFloatValue(_:)` so `CGFloat`/`Int64`/etc don't silently scale to zero. - Removing code: confirm it's not reachable from the Public Contract and not referenced in `Tests/` or `Examples/`. - Verify with `swift build` **and** `swift test` on macOS (catches the platform-isolation regressions). -## Known Issues (not yet addressed) - -- **Data race**: `_scaleWidth/_scaleHeight/_scaleText` are plain `var` read without synchronization (only `@unchecked Sendable` hides it). TSan would flag. A proper fix needs atomic reads. -- **Stale on rotation**: caches invalidate on orientation change, but scale factors only recompute on `configure()`/`refreshMetrics()`. No observer re-derives `_scale*` after rotation. -- **`fastW` ≈ `.w`**: measured ~1.1× faster, not the README's "6×". For real hot loops use `FastScale` (capture-once). -- **README drift**: README documents APIs that don't exist (`.ssp`, `adaptiveFont`, `isIPad`, `pixelRatio`, `orientation`, `bottomSafeArea`, `prewarmCaches`, `ResponsiveGrid`, `fontResolver`, `debugMode`). Reconcile before publishing. - ## Build & Test ```bash diff --git a/Examples/README.md b/Examples/README.md index 449ad27..66b4fe5 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -33,20 +33,15 @@ Complete SwiftUI implementation examples including: #### Basic SwiftUI Views - **BasicSwiftUIView**: Core SwiftUI usage patterns - - `.scaledSystem()` font modifier - - `.responsivePadding()` and `.responsiveCornerRadius()` modifiers + - `.font(.system(size: 16.sp))` — scale the size with `.sp` inside native font APIs + - `.frame(width: 200.w, height: 50.h)` / `.padding(.horizontal, 20.w)` — scale inside native modifiers - Grid layouts with responsive spacing - Screen metrics display #### Advanced SwiftUI Features -- **PropertyWrappersDemo**: Property wrapper usage - - `@ScaledValue` for automatic value scaling - - `@ScreenPercentage` for percentage-based sizing - - `@ResponsiveFont` for adaptive typography - -- **ViewModifiersDemo**: Custom view modifiers - - `.responsiveFrame()` modifier - - `.responsivePadding()` modifier +- **ViewModifiersDemo**: Scaling values inside native SwiftUI modifiers + - `.frame(width: 200.w, height: 100.h)` + - `.clipShape(RoundedRectangle(cornerRadius: 12.r, style: .continuous))` - Animated scaling examples #### Custom Components @@ -65,7 +60,7 @@ label.font = .systemFont(ofSize: 16.sp) view.layer.cornerRadius = 8.r // SwiftUI -Text("Hello").font(.scaledSystem(size: 16)) +Text("Hello").font(.system(size: 16.sp)) Rectangle().frame(width: 100.w, height: 50.h) ``` @@ -86,8 +81,8 @@ ScreenUtil.shared.configure(with: config) ### 3. Batch Operations ```swift let values = [10, 20, 30, 40, 50] -let scaledWidths = ScreenUtil.shared.batchWidths(values) -let scaledHeights = ScreenUtil.shared.batchHeights(values) +let scaledWidths = ScreenUtil.shared.batchScaler.widths(values) +let scaledHeights = ScreenUtil.shared.batchScaler.heights(values) ``` ### 4. Fast Scaling @@ -108,13 +103,10 @@ print("Device: \(ScreenUtil.shared.deviceType)") ### 6. Debug Tools ```swift // Print current configuration -ScreenUtil.shared.debug.printCurrentConfiguration() +ScreenUtilDebug.printCurrentConfiguration() // Run performance benchmark -ScreenUtil.shared.debug.benchmarkScalingOperations() - -// Generate test report -let report = ScreenUtil.shared.debug.generateTestReport() +ScreenUtilDebug.benchmarkScalingOperations() ``` ## Usage Instructions diff --git a/Examples/Sources/SwiftUIExample/main.swift b/Examples/Sources/SwiftUIExample/main.swift index c9bf4ea..305b89a 100644 --- a/Examples/Sources/SwiftUIExample/main.swift +++ b/Examples/Sources/SwiftUIExample/main.swift @@ -44,7 +44,7 @@ struct BasicSwiftUIView: View { screenUtil.configure(with: .iPhone13Pro) // Optional: Print current configuration for debugging - screenUtil.debug.printCurrentConfiguration() + ScreenUtilDebug.printCurrentConfiguration() } // MARK: - Header Section @@ -56,11 +56,11 @@ struct BasicSwiftUIView: View { .foregroundColor(.blue) Text("Responsive Design") - .font(.scaledSystem(size: 28, weight: .bold)) + .font(.system(size: (28).sp, weight: .bold)) .multilineTextAlignment(.center) - + Text("Built with ScreenUtil for perfect adaptation across all devices") - .font(.scaledSystem(size: 16)) + .font(.system(size: (16).sp)) .foregroundColor(.secondary) .multilineTextAlignment(.center) .lineLimit(3) @@ -79,7 +79,7 @@ struct BasicSwiftUIView: View { VStack(spacing: 16.h) { HStack { Text("Features") - .font(.scaledSystem(size: 22, weight: .semibold)) + .font(.system(size: (22).sp, weight: .semibold)) Spacer() } @@ -120,7 +120,7 @@ struct BasicSwiftUIView: View { VStack(spacing: 20.h) { HStack { Text("Interactive Demo") - .font(.scaledSystem(size: 22, weight: .semibold)) + .font(.system(size: (22).sp, weight: .semibold)) Spacer() } @@ -150,7 +150,7 @@ struct BasicSwiftUIView: View { Spacer() Image(systemName: "chevron.right") } - .font(.scaledSystem(size: 16, weight: .medium)) + .font(.system(size: (16).sp, weight: .medium)) .padding(.horizontal, 16.w) .padding(.vertical, 12.h) .background( @@ -167,7 +167,7 @@ struct BasicSwiftUIView: View { private var infoSection: some View { VStack(alignment: .leading, spacing: 12.h) { Text("Screen Metrics") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) InfoRow(label: "Screen Size", value: "\(Int(ScreenUtil.shared.getScreenMetrics().width)) x \(Int(ScreenUtil.shared.getScreenMetrics().height)) pts") InfoRow(label: "Scale Factor", value: "\(ScreenUtil.shared.getScreenMetrics().scale)x") @@ -200,11 +200,11 @@ struct FeatureCard: View { .foregroundColor(.blue) Text(title) - .font(.scaledSystem(size: 14, weight: .semibold)) + .font(.system(size: (14).sp, weight: .semibold)) .lineLimit(1) - + Text(description) - .font(.scaledSystem(size: 12)) + .font(.system(size: (12).sp)) .foregroundColor(.secondary) .multilineTextAlignment(.center) .lineLimit(2) @@ -234,7 +234,7 @@ struct ResponsiveButton: View { var body: some View { Button(action: action) { Text(title) - .font(.scaledSystem(size: 16, weight: .medium)) + .font(.system(size: (16).sp, weight: .medium)) .foregroundColor(style == .primary ? .white : .blue) .frame(maxWidth: .infinity) .frame(height: 44.h) @@ -259,7 +259,7 @@ struct ScalingDemoView: View { var body: some View { VStack(spacing: 16.h) { Text("Scaling Types Demo") - .font(.scaledSystem(size: 16, weight: .medium)) + .font(.system(size: (16).sp, weight: .medium)) // Scale type picker Picker("Scale Type", selection: $selectedScale) { @@ -282,7 +282,7 @@ struct ScalingDemoView: View { .frame(width: 20.w, height: scaledValue) Text("\(Int(scaledValue))") - .font(.scaledSystem(size: 10)) + .font(.system(size: (10).sp)) .foregroundColor(.secondary) } } @@ -306,13 +306,13 @@ struct InfoRow: View { var body: some View { HStack { Text(label) - .font(.scaledSystem(size: 14)) + .font(.system(size: (14).sp)) .foregroundColor(.secondary) - + Spacer() - + Text(value) - .font(.scaledSystem(size: 14, weight: .medium)) + .font(.system(size: (14).sp, weight: .medium)) } } } @@ -323,7 +323,6 @@ struct AdvancedSwiftUIView: View { var body: some View { NavigationView { List { - NavigationLink("Property Wrappers", destination: PropertyWrappersDemo()) NavigationLink("View Modifiers", destination: ViewModifiersDemo()) NavigationLink("Responsive Layout", destination: ResponsiveLayoutDemo()) NavigationLink("Custom Components", destination: CustomComponentsDemo()) @@ -333,80 +332,6 @@ struct AdvancedSwiftUIView: View { } } -// MARK: - Property Wrappers Demo - -struct PropertyWrappersDemo: View { - @ScaledValue(wrappedValue: 100, .width) private var scaledWidth - @ScaledValue(wrappedValue:50, .height) private var scaledHeight - @ScaledValue(wrappedValue:16, .font) private var scaledFontSize - @ScreenPercentage(wrappedValue:25, .width) private var screenPercentage - - var body: some View { - VStack(spacing: 24.h) { - Text("Property Wrappers Demo") - .font(.scaledSystem(size: 24, weight: .bold)) - - VStack(alignment: .leading, spacing: 16.h) { - PropertyWrapperCard( - title: "@ScaledValue", - description: "Automatically scales values based on screen size", - example: "Width: \(Int(scaledWidth)), Height: \(Int(scaledHeight))" - ) - - PropertyWrapperCard( - title: "@ScreenPercentage", - description: "Uses percentage of screen dimensions", - example: "25% of screen width: \(Int(screenPercentage))" - ) - - PropertyWrapperCard( - title: "@ResponsiveFont", - description: "Scales font size automatically", - example: "Font size: \(Int(scaledFontSize))sp" - ) - } - - Spacer() - } - .padding(20.w) - .navigationTitle("Property Wrappers") - .navigationBarTitleDisplayMode(.inline) - } -} - -struct PropertyWrapperCard: View { - let title: String - let description: String - let example: String - - var body: some View { - VStack(alignment: .leading, spacing: 8.h) { - Text(title) - .font(.scaledSystem(size: 16, weight: .semibold)) - .foregroundColor(.blue) - - Text(description) - .font(.scaledSystem(size: 14)) - .foregroundColor(.secondary) - - Text(example) - .font(.scaledSystem(size: 12, design: .monospaced)) - .foregroundColor(.green) - .padding(.horizontal, 12.w) - .padding(.vertical, 6.h) - .background( - RoundedRectangle(cornerRadius: 4.r) - .fill(Color.green.opacity(0.1)) - ) - } - .padding(16.w) - .background( - RoundedRectangle(cornerRadius: 12.r) - .fill(Color(.secondarySystemBackground)) - ) - } -} - // MARK: - View Modifiers Demo struct ViewModifiersDemo: View { @@ -414,25 +339,25 @@ struct ViewModifiersDemo: View { ScrollView { VStack(spacing: 24.h) { Text("View Modifiers Demo") - .font(.scaledSystem(size: 24, weight: .bold)) - + .font(.system(size: (24).sp, weight: .bold)) + // Responsive padding Text("Responsive Padding") - .font(.scaledSystem(size: 16, weight: .medium)) + .font(.system(size: (16).sp, weight: .medium)) .padding(.horizontal, 20.w) .padding(.vertical, 8.w) .background(Color.blue.opacity(0.1)) - .responsiveCornerRadius(8) + .clipShape(RoundedRectangle(cornerRadius: 8.r, style: .continuous)) // Responsive frame Rectangle() .fill(Color.green) - .responsiveFrame(width: 200, height: 100) - .responsiveCornerRadius(12) + .frame(width: 200.w, height: 100.h) + .clipShape(RoundedRectangle(cornerRadius: 12.r, style: .continuous)) // Custom responsive modifier Text("Custom Modifier") - .font(.scaledSystem(size: 14)) + .font(.system(size: (14).sp)) .responsiveCard() // Animated scaling @@ -453,7 +378,7 @@ struct AnimatedScalingExample: View { var body: some View { VStack(spacing: 16.h) { Text("Animated Scaling") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) Circle() .fill(Color.purple) @@ -466,12 +391,12 @@ struct AnimatedScalingExample: View { Button("Toggle Scale") { isScaled.toggle() } - .font(.scaledSystem(size: 16, weight: .medium)) + .font(.system(size: (16).sp, weight: .medium)) .foregroundColor(.white) .padding(.horizontal, 24.w) .padding(.vertical, 12.h) .background(Color.purple) - .responsiveCornerRadius(8) + .clipShape(RoundedRectangle(cornerRadius: 8.r, style: .continuous)) } } } @@ -483,7 +408,7 @@ struct ResponsiveLayoutDemo: View { ScrollView { VStack(spacing: 24.h) { Text("Responsive Layout") - .font(.scaledSystem(size: 24, weight: .bold)) + .font(.system(size: (24).sp, weight: .bold)) // Responsive grid ResponsiveGrid() @@ -515,7 +440,7 @@ struct ResponsiveGrid: View { var body: some View { VStack(alignment: .leading, spacing: 12.h) { Text("Responsive Grid") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) LazyVGrid(columns: columns, spacing: 8.h) { ForEach(items, id: \.self) { item in @@ -524,7 +449,7 @@ struct ResponsiveGrid: View { .aspectRatio(1, contentMode: .fit) .overlay( Text("\(item)") - .font(.scaledSystem(size: 14, weight: .bold)) + .font(.system(size: (14).sp, weight: .bold)) .foregroundColor(.white) ) } @@ -537,7 +462,7 @@ struct AdaptiveStackExample: View { var body: some View { VStack(alignment: .leading, spacing: 12.h) { Text("Adaptive Stack") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) // This would adapt based on screen size in a real implementation HStack(spacing: 12.w) { @@ -548,7 +473,7 @@ struct AdaptiveStackExample: View { .frame(width: 40.w, height: 40.h) Text("Item \(index)") - .font(.scaledSystem(size: 12)) + .font(.system(size: (12).sp)) } .frame(maxWidth: .infinity) .padding(.vertical, 16.h) @@ -566,7 +491,7 @@ struct ResponsiveSpacingExample: View { var body: some View { VStack(alignment: .leading, spacing: 12.h) { Text("Responsive Spacing") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) VStack(spacing: 16.h) { ForEach(1...4, id: \.self) { index in @@ -576,7 +501,7 @@ struct ResponsiveSpacingExample: View { .frame(width: 4.w, height: 20.h) Text("Spacing example \(index)") - .font(.scaledSystem(size: 14)) + .font(.system(size: (14).sp)) Spacer() } @@ -595,11 +520,11 @@ struct PerformanceDemoView: View { var body: some View { VStack(spacing: 24.h) { Text("Performance Testing") - .font(.scaledSystem(size: 24, weight: .bold)) + .font(.system(size: (24).sp, weight: .bold)) Button(action: runPerformanceTest) { Text(isRunning ? "Running..." : "Run Performance Test") - .font(.scaledSystem(size: 16, weight: .medium)) + .font(.system(size: (16).sp, weight: .medium)) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 44.h) @@ -613,7 +538,7 @@ struct PerformanceDemoView: View { if !results.isEmpty { ScrollView { Text(results) - .font(.scaledSystem(size: 12, design: .monospaced)) + .font(.system(size: (12).sp, design: .monospaced)) .padding(16.w) .background( RoundedRectangle(cornerRadius: 8.r) @@ -656,7 +581,7 @@ struct PerformanceDemoView: View { let batchTest = measureTime { let values = Array(1...1000) for _ in 0..<100 { - _ = ScreenUtil.shared.batchWidths(values) + _ = ScreenUtil.shared.batchScaler.widths(values) } } @@ -693,7 +618,7 @@ struct CustomComponentsDemo: View { ScrollView { VStack(spacing: 24.h) { Text("Custom Components") - .font(.scaledSystem(size: 24, weight: .bold)) + .font(.system(size: (24).sp, weight: .bold)) ResponsiveProfileCard() @@ -714,7 +639,7 @@ struct ResponsiveProfileCard: View { var body: some View { VStack(spacing: 16.h) { Text("Profile Card") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) HStack(spacing: 16.w) { Circle() @@ -728,23 +653,23 @@ struct ResponsiveProfileCard: View { .frame(width: 60.w, height: 60.h) .overlay( Text("JD") - .font(.scaledSystem(size: 20, weight: .bold)) + .font(.system(size: (20).sp, weight: .bold)) .foregroundColor(.white) ) VStack(alignment: .leading, spacing: 4.h) { Text("John Doe") - .font(.scaledSystem(size: 16, weight: .semibold)) - + .font(.system(size: (16).sp, weight: .semibold)) + Text("iOS Developer") - .font(.scaledSystem(size: 14)) + .font(.system(size: (14).sp)) .foregroundColor(.secondary) HStack(spacing: 4.w) { Image(systemName: "location.fill") .font(.system(size: 10.sp)) Text("San Francisco, CA") - .font(.scaledSystem(size: 12)) + .font(.system(size: (12).sp)) } .foregroundColor(.secondary) } @@ -767,17 +692,17 @@ struct ResponsiveProgressBar: View { var body: some View { VStack(spacing: 16.h) { Text("Progress Bar") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) VStack(alignment: .leading, spacing: 8.h) { HStack { Text("Project Progress") - .font(.scaledSystem(size: 14, weight: .medium)) - + .font(.system(size: (14).sp, weight: .medium)) + Spacer() - + Text("\(Int(progress * 100))%") - .font(.scaledSystem(size: 12, weight: .medium)) + .font(.system(size: (12).sp, weight: .medium)) .foregroundColor(.blue) } @@ -810,7 +735,7 @@ struct ResponsiveProgressBar: View { Button("Update Progress") { progress = Double.random(in: 0.3...1.0) } - .font(.scaledSystem(size: 14, weight: .medium)) + .font(.system(size: (14).sp, weight: .medium)) } } } @@ -826,7 +751,7 @@ struct ResponsiveStatsView: View { var body: some View { VStack(spacing: 16.h) { Text("Statistics") - .font(.scaledSystem(size: 18, weight: .semibold)) + .font(.system(size: (18).sp, weight: .semibold)) LazyVGrid(columns: [ GridItem(.flexible(), spacing: 8.w), @@ -837,11 +762,11 @@ struct ResponsiveStatsView: View { VStack(spacing: 8.h) { Text(value) - .font(.scaledSystem(size: 24, weight: .bold)) + .font(.system(size: (24).sp, weight: .bold)) .foregroundColor(color) - + Text(title) - .font(.scaledSystem(size: 12)) + .font(.system(size: (12).sp)) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) diff --git a/Examples/Sources/UIKitExample/main.swift b/Examples/Sources/UIKitExample/main.swift index fff91f6..79f2a54 100644 --- a/Examples/Sources/UIKitExample/main.swift +++ b/Examples/Sources/UIKitExample/main.swift @@ -209,7 +209,7 @@ class BasicUIKitViewController: UIViewController { @objc private func buttonTapped(_ sender: UIButton) { // Demonstrate debug functionality - ScreenUtil.shared.debug.printCurrentConfiguration() + ScreenUtilDebug.printCurrentConfiguration() // Show alert with responsive sizing let alert = UIAlertController( @@ -278,8 +278,8 @@ class AdvancedUIKitViewController: UIViewController { // Demonstrate batch scaling for multiple values let values = [10, 20, 30, 40, 50] - let scaledWidths = ScreenUtil.shared.batchWidths(values) - let scaledHeights = ScreenUtil.shared.batchHeights(values) + let scaledWidths = ScreenUtil.shared.batchScaler.widths(values) + let scaledHeights = ScreenUtil.shared.batchScaler.heights(values) // Create visual representation let containerView = UIView() @@ -457,7 +457,7 @@ class AdvancedUIKitViewController: UIViewController { @objc private func runPerformanceTest() { // Run performance benchmark - ScreenUtil.shared.debug.benchmarkScalingOperations() + ScreenUtilDebug.benchmarkScalingOperations() // Show results in alert let alert = UIAlertController( diff --git a/README.md b/README.md index 428cc1e..0cd5b05 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ - 🔒 **Thread-Safe** — lock-free reads via an atomic scale-factor snapshot (no torn reads) - 📱 **Design-Based Scaling** — scale UI relative to your design dimensions - 🎯 **Simple API** — intuitive extensions: `.w`, `.h`, `.sp`, `.r` -- ⚡ **Fast Path** — capture-once `FastScale` for hot loops; `.fastW/.fastH/.fastSp` for quick reads -- 📦 **Batch Scaling** — `BatchScaler` / `batch*` for bulk work +- ⚡ **Fast Path** — capture-once `FastScale` for hot loops +- 📦 **Batch Scaling** — `BatchScaler` / `withBatchScaler` for bulk work - 🔧 **UIKit & SwiftUI** — first-class support for both - 📊 **Percentage Sizing** — `.sw` / `.sh` for screen-relative layouts - 🖥️ **Multi-platform** — iOS, macOS, tvOS, watchOS @@ -73,8 +73,8 @@ button.layer.cornerRadius = 12.r // SwiftUI Text("Hello World") .font(.system(size: 16.sp)) - .responsiveFrame(width: 200, height: 50) - .responsivePadding(.all, 20) + .frame(width: 200.w, height: 50.h) + .padding(.horizontal, 20.w).padding(.vertical, 20.h) ``` ## 📖 Usage @@ -110,13 +110,11 @@ let fast = ScreenUtil.shared.fastScale view.frame = fast.rect(designRect) ``` -Per-value `.fastW` / `.fastH` / `.fastSp` skip the input validation but are only marginally faster than `.w`; prefer `FastScale` when you actually have a hot loop. - ### Batch scaling ```swift let widths: [CGFloat] = [100, 200, 300, 400] -let scaled = ScreenUtil.shared.batchWidths(widths) // one factor lookup for all +let scaled = ScreenUtil.shared.batchScaler.widths(widths) // one factor lookup for all // Reusable scaler: let scaler = ScreenUtil.shared.batchScaler @@ -124,7 +122,7 @@ let heights = scaler.heights([10, 20, 30]) let fonts = scaler.fontSizes([12, 14, 16]) ``` -`batch*` and `BatchScaler` accept any `[T: Numeric]` (`Int`, `Double`, `CGFloat`, `Int64`, …). +`BatchScaler` accepts any `[T: Numeric]` (`Int`, `Double`, `CGFloat`, `Int64`, …). ### Scaling limits @@ -175,38 +173,43 @@ func collectionView(_ collectionView: UICollectionView, ## 🎨 SwiftUI -### View modifiers +### Scaling inside native modifiers + +Apply the `.w/.h/.sp/.r` properties directly inside SwiftUI's own modifiers — no wrappers needed: ```swift Image(systemName: "star.fill") - .responsiveFrame(width: 60, height: 60) + .frame(width: 60.w, height: 60.h) VStack { - Text("Welcome").font(.scaledSystem(size: 28, weight: .bold)) + Text("Welcome").font(.system(size: 28.sp, weight: .bold)) } -.responsivePadding(.all, 20) -.responsiveCornerRadius(16) +.padding(.horizontal, 20.w).padding(.vertical, 20.h) +.clipShape(RoundedRectangle(cornerRadius: 16.r, style: .continuous)) ``` ### Fonts +Apply `.sp` to the size inside the native font APIs: + ```swift -Text("Title").font(.scaledSystem(size: 24, weight: .bold)) -Text("Body").font(.scaledCustom("Helvetica", size: 16)) +Text("Title").font(.system(size: 24.sp, weight: .bold)) +Text("Body").font(.custom("Helvetica", size: 16.sp)) ``` -### Property wrappers +### Scaling values + +Keep design values as plain constants and scale them at the call site with `.w/.h/.sp/.sw`: ```swift struct CardView: View { - @ScaledValue(.width) private var cardWidth = 300 - @ScaledValue(.height) private var cardHeight = 200 - @ScaledValue(.font) private var titleSize = 24 - @ScreenPercentage(.width) private var halfWidth = 50 // 50% of screen width + private let cardWidth: CGFloat = 300 + private let cardHeight: CGFloat = 200 + private let titleSize: CGFloat = 24 var body: some View { - VStack { /* ... */ } - .frame(width: cardWidth, height: cardHeight) + VStack { Text("Card").font(.system(size: titleSize.sp)) } + .frame(width: cardWidth.w, height: cardHeight.h) } } ``` @@ -229,7 +232,6 @@ struct CardView: View { | `.r` | Radius scaling | `12.r` | | `.sw` | Screen width percentage | `50.sw` | | `.sh` | Screen height percentage | `10.sh` | -| `.fastW` `.fastH` `.fastSp` | Fast (unvalidated) variants | `100.fastW` | ### `ScreenUtil.shared` @@ -258,13 +260,11 @@ var batchScaler: BatchScaler ## 🐛 Debugging ```swift -ScreenUtil.shared.debug.printCurrentConfiguration() -_ = ScreenUtil.shared.debug.validateScaling() -let report = ScreenUtil.shared.debug.generateTestReport() +ScreenUtilDebug.printCurrentConfiguration() #if DEBUG -ScreenUtil.shared.debug.benchmarkScalingOperations() // DEBUG builds only -ScreenUtil.shared.debug.showDebugOverlay(on: view) // UIKit +ScreenUtilDebug.benchmarkScalingOperations() // DEBUG builds only +ScreenUtilDebug.showDebugOverlay(on: view) // UIKit #endif ``` diff --git a/Sources/ScreenUtil/Core/ScreenUtil.swift b/Sources/ScreenUtil/Core/ScreenUtil.swift index 104ed94..c272a42 100644 --- a/Sources/ScreenUtil/Core/ScreenUtil.swift +++ b/Sources/ScreenUtil/Core/ScreenUtil.swift @@ -157,7 +157,4 @@ public extension ScreenUtil { @inline(__always) func r(_ value: CGFloat) -> CGFloat { scale(for: value, scaleType: .radius) } @inline(__always) func sw(_ percentage: CGFloat) -> CGFloat { screenWidth * (percentage / 100.0) } @inline(__always) func sh(_ percentage: CGFloat) -> CGFloat { screenHeight * (percentage / 100.0) } - @inline(__always) func fastW(_ value: CGFloat) -> CGFloat { value * _snapshot.scaleWidth } - @inline(__always) func fastH(_ value: CGFloat) -> CGFloat { value * _snapshot.scaleHeight } - @inline(__always) func fastSp(_ value: CGFloat) -> CGFloat { value * _snapshot.scaleText } } diff --git a/Sources/ScreenUtil/Debug/ScreenUtilDebug.swift b/Sources/ScreenUtil/Debug/ScreenUtilDebug.swift index 0d684b1..25c349e 100644 --- a/Sources/ScreenUtil/Debug/ScreenUtilDebug.swift +++ b/Sources/ScreenUtil/Debug/ScreenUtilDebug.swift @@ -13,14 +13,9 @@ import UIKit #endif /// Debug and development utilities for inspecting ScreenUtil's active configuration and scaling behaviour. -public struct ScreenUtilDebug: Sendable { - /// Shared singleton instance. - public static let shared = ScreenUtilDebug() - - private init() {} - +public enum ScreenUtilDebug { /// Logs the current screen metrics and scale factors via `os.Logger`. - public func printCurrentConfiguration() { + public static func printCurrentConfiguration() { let screenUtil = ScreenUtil.shared let metrics = screenUtil.getScreenMetrics() @@ -40,7 +35,7 @@ public struct ScreenUtilDebug: Sendable { #if DEBUG /// Runs `operation` `iterations` times and returns the last result alongside the average execution time. - public func measurePerformance(_ operation: () -> T, iterations: Int = 10000) -> (result: T, averageTime: TimeInterval) { + public static func measurePerformance(_ operation: () -> T, iterations: Int = 10000) -> (result: T, averageTime: TimeInterval) { var totalTime: TimeInterval = 0 var result: T! @@ -55,8 +50,8 @@ public struct ScreenUtilDebug: Sendable { return (result, averageTime) } - /// Benchmarks standard, fast, and batch scaling paths and logs the results. - public func benchmarkScalingOperations() { + /// Benchmarks standard, FastScale, and batch scaling paths and logs the results. + public static func benchmarkScalingOperations() { let testValue: CGFloat = 100.0 let iterations = 100000 @@ -67,100 +62,25 @@ public struct ScreenUtilDebug: Sendable { }, iterations: iterations) let fastW = measurePerformance({ - testValue.fastW + withFastScale { $0.width(testValue) } }, iterations: iterations) let batchOperation = measurePerformance({ - ScreenUtil.shared.batchWidths([10, 20, 30, 40, 50]) + withBatchScaler { $0.widths([10, 20, 30, 40, 50]) } }, iterations: iterations / 10) Log(.benchmark, "Standard .w: \(String(format: "%.2f", standardW.averageTime * 1_000_000)) μs") - Log(.benchmark, "Fast .fastW: \(String(format: "%.2f", fastW.averageTime * 1_000_000)) μs") + Log(.benchmark, "FastScale .width: \(String(format: "%.2f", fastW.averageTime * 1_000_000)) μs") Log(.benchmark, "Batch operation: \(String(format: "%.2f", batchOperation.averageTime * 1_000_000)) μs") let speedup = standardW.averageTime / fastW.averageTime - Log(.benchmark, "Fast path is \(String(format: "%.1f", speedup))x faster") + Log(.benchmark, "FastScale path is \(String(format: "%.1f", speedup))x faster") } #endif - /// Validates that all scaling operations produce positive results for a set of test inputs; returns `true` when all pass. - public func validateScaling() -> Bool { - let screenUtil = ScreenUtil.shared - var allValid = true - - let testCases: [(input: CGFloat, expected: String)] = [ - (100, "Width scaling"), - (50, "Height scaling"), - (16, "Font scaling"), - (12, "Radius scaling") - ] - - Log(.debug, "Validation Tests:") - - for testCase in testCases { - let widthResult = screenUtil.w(testCase.input) - let heightResult = screenUtil.h(testCase.input) - let textResult = screenUtil.sp(testCase.input) - let radiusResult = screenUtil.r(testCase.input) - - let isValid = widthResult > 0 && heightResult > 0 && textResult > 0 && radiusResult > 0 - if !isValid { - allValid = false - Log(.debug, "\(testCase.expected) failed for input \(testCase.input)", level: .error) - } else { - Log(.debug, "\(testCase.expected) passed") - } - } - - return allValid - } - - /// Generates a markdown-formatted test report containing device info, scale factors, and sample scaled values. - public func generateTestReport() -> String { - let screenUtil = ScreenUtil.shared - let metrics = screenUtil.getScreenMetrics() - let timestamp = Date() - - return """ - # ScreenUtil Test Report - - **Generated:** \(timestamp) - - ## Device Information - - Screen Size: \(metrics.width) x \(metrics.height) pts - - Scale Factor: \(metrics.scale)x - - Device Type: \(screenUtil.deviceType) - - ## Scaling Factors - - Width Scale: \(screenUtil.scaleWidth) - - Height Scale: \(screenUtil.scaleHeight) - - Text Scale: \(screenUtil.scaleText) - - ## Safe Area - - Top: \(metrics.safeAreaInsets.0) pts - - Bottom: \(metrics.safeAreaInsets.1) pts - - Left: \(metrics.safeAreaInsets.2) pts - - Right: \(metrics.safeAreaInsets.3) pts - - ## Status Bar - - Height: \(metrics.statusBarHeight) pts - - ## Test Values - | Input | Width (.w) | Height (.h) | Font (.sp) | Radius (.r) | - |-------|------------|-------------|------------|--------------| - | 10 | \(screenUtil.w(10)) | \(screenUtil.h(10)) | \(screenUtil.sp(10)) | \(screenUtil.r(10)) | - | 20 | \(screenUtil.w(20)) | \(screenUtil.h(20)) | \(screenUtil.sp(20)) | \(screenUtil.r(20)) | - | 50 | \(screenUtil.w(50)) | \(screenUtil.h(50)) | \(screenUtil.sp(50)) | \(screenUtil.r(50)) | - | 100 | \(screenUtil.w(100)) | \(screenUtil.h(100)) | \(screenUtil.sp(100)) | \(screenUtil.r(100)) | - - --- - Generated by ScreenUtil Debug - """ - } - #if canImport(UIKit) && DEBUG /// Adds a semi-transparent overlay showing current screen metrics and scale factors to `view`. - @MainActor public func showDebugOverlay(on view: UIView) { + @MainActor public static func showDebugOverlay(on view: UIView) { let overlayView = createDebugOverlay() view.addSubview(overlayView) @@ -173,7 +93,7 @@ public struct ScreenUtilDebug: Sendable { ]) } - @MainActor private func createDebugOverlay() -> UIView { + @MainActor private static func createDebugOverlay() -> UIView { let containerView = UIView() containerView.backgroundColor = UIColor.black.withAlphaComponent(0.8) containerView.layer.cornerRadius = 8.r @@ -214,22 +134,3 @@ public struct ScreenUtilDebug: Sendable { } #endif } - -public extension ScreenUtil { - /// Convenience accessor for `ScreenUtilDebug.shared`. - var debug: ScreenUtilDebug { - return ScreenUtilDebug.shared - } -} - -#if DEBUG -/// Prints the current ScreenUtil configuration to the log; available in DEBUG builds only. -public func printScreenUtilInfo() { - ScreenUtilDebug.shared.printCurrentConfiguration() -} - -/// Runs the ScreenUtil scaling benchmark and logs results; available in DEBUG builds only. -public func benchmarkScreenUtil() { - ScreenUtilDebug.shared.benchmarkScalingOperations() -} -#endif diff --git a/Sources/ScreenUtil/Scaling/BatchScaling.swift b/Sources/ScreenUtil/Scaling/BatchScaling.swift index b0b30a4..88da30f 100644 --- a/Sources/ScreenUtil/Scaling/BatchScaling.swift +++ b/Sources/ScreenUtil/Scaling/BatchScaling.swift @@ -12,88 +12,6 @@ import CoreGraphics import UIKit #endif -public extension ScreenUtil { - /// Scales an array of numeric values using the specified `scaleType`, returning scaled `CGFloat` values. - @inline(__always) - func batchScale(_ values: [T], scaleType: ScaleType) -> [CGFloat] { - let cache = ScaleFactorCache(from: self) - return values.map { value in - let cgValue = cgFloatValue(value) - switch scaleType { - case .width: return cache.scaleWidth(cgValue) - case .height: return cache.scaleHeight(cgValue) - case .text: return cache.scaleText(cgValue) - case .radius: return cache.scaleRadius(cgValue) - } - } - } - - /// Scales an array of numeric values by the width factor, returning scaled `CGFloat` values. - @inline(__always) - func batchWidths(_ values: [T]) -> [CGFloat] { - let cache = ScaleFactorCache(from: self) - return values.map { cache.scaleWidth(cgFloatValue($0)) } - } - - /// Scales an array of numeric values by the height factor, returning scaled `CGFloat` values. - @inline(__always) - func batchHeights(_ values: [T]) -> [CGFloat] { - let cache = ScaleFactorCache(from: self) - return values.map { cache.scaleHeight(cgFloatValue($0)) } - } - - /// Scales an array of numeric values as font sizes using the text scale factor, returning scaled `CGFloat` values. - @inline(__always) - func batchFontSizes(_ values: [T]) -> [CGFloat] { - let cache = ScaleFactorCache(from: self) - return values.map { cache.scaleText(cgFloatValue($0)) } - } - - /// Scales an array of `CGSize` values, applying width and height factors independently. - @inline(__always) - func batchSizes(_ sizes: [CGSize]) -> [CGSize] { - let cache = ScaleFactorCache(from: self) - return sizes.map { CGSize(width: cache.scaleWidth($0.width), height: cache.scaleHeight($0.height)) } - } - - /// Scales an array of `CGPoint` values, applying width factor to x and height factor to y. - @inline(__always) - func batchPoints(_ points: [CGPoint]) -> [CGPoint] { - let cache = ScaleFactorCache(from: self) - return points.map { CGPoint(x: cache.scaleWidth($0.x), y: cache.scaleHeight($0.y)) } - } - - /// Scales an array of `CGRect` values, applying width and height factors to origin and size. - @inline(__always) - func batchRects(_ rects: [CGRect]) -> [CGRect] { - let cache = ScaleFactorCache(from: self) - return rects.map { rect in - CGRect( - x: cache.scaleWidth(rect.origin.x), - y: cache.scaleHeight(rect.origin.y), - width: cache.scaleWidth(rect.size.width), - height: cache.scaleHeight(rect.size.height) - ) - } - } - - #if canImport(UIKit) - /// Scales an array of `UIEdgeInsets` values, applying width factor to left/right and height factor to top/bottom. - @inline(__always) - func batchEdgeInsets(_ insets: [UIEdgeInsets]) -> [UIEdgeInsets] { - let cache = ScaleFactorCache(from: self) - return insets.map { inset in - UIEdgeInsets( - top: cache.scaleHeight(inset.top), - left: cache.scaleWidth(inset.left), - bottom: cache.scaleHeight(inset.bottom), - right: cache.scaleWidth(inset.right) - ) - } - } - #endif -} - /// Reusable batch-scaling context that captures scale factors once for efficient repeated scaling. public struct BatchScaler: Sendable { private let cache: ScaleFactorCache @@ -126,12 +44,41 @@ public struct BatchScaler: Sendable { return values.map { cache.scaleRadius(cgFloatValue($0)) } } + /// Scales an array of numeric values by the captured factor for `scaleType`. + @inline(__always) + public func scale(_ values: [T], scaleType: ScaleType) -> [CGFloat] { + values.map { value in + let v = cgFloatValue(value) + switch scaleType { + case .width: return cache.scaleWidth(v) + case .height: return cache.scaleHeight(v) + case .text: return cache.scaleText(v) + case .radius: return cache.scaleRadius(v) + } + } + } + /// Scales an array of `CGSize` values using the captured width and height factors. @inline(__always) public func sizes(_ sizes: [CGSize]) -> [CGSize] { return sizes.map { CGSize(width: cache.scaleWidth($0.width), height: cache.scaleHeight($0.height)) } } + /// Scales an array of `CGPoint` values using the captured width and height factors. + @inline(__always) + public func points(_ points: [CGPoint]) -> [CGPoint] { + points.map { CGPoint(x: cache.scaleWidth($0.x), y: cache.scaleHeight($0.y)) } + } + + /// Scales an array of `CGRect` values using the captured width and height factors. + @inline(__always) + public func rects(_ rects: [CGRect]) -> [CGRect] { + rects.map { + CGRect(x: cache.scaleWidth($0.origin.x), y: cache.scaleHeight($0.origin.y), + width: cache.scaleWidth($0.size.width), height: cache.scaleHeight($0.size.height)) + } + } + #if canImport(UIKit) /// Scales an array of `UIEdgeInsets` values using the captured width and height factors. @inline(__always) diff --git a/Sources/ScreenUtil/Scaling/FastScale.swift b/Sources/ScreenUtil/Scaling/FastScale.swift index 8eb1b0a..fcd4823 100644 --- a/Sources/ScreenUtil/Scaling/FastScale.swift +++ b/Sources/ScreenUtil/Scaling/FastScale.swift @@ -11,60 +11,48 @@ import CoreGraphics /// Capture-once scaling context that pre-stores scale factors for use in hot loops. public struct FastScale: Sendable { - private let widthScale: CGFloat - private let heightScale: CGFloat - private let textScale: CGFloat + private let cache: ScaleFactorCache - internal init(widthScale: CGFloat, heightScale: CGFloat, textScale: CGFloat) { - self.widthScale = widthScale - self.heightScale = heightScale - self.textScale = textScale + internal init(cache: ScaleFactorCache) { + self.cache = cache } /// Returns `value` scaled by the captured width factor. @inline(__always) - public func width(_ value: CGFloat) -> CGFloat { - return value * widthScale - } + public func width(_ value: CGFloat) -> CGFloat { cache.scaleWidth(value) } /// Returns `value` scaled by the captured height factor. @inline(__always) - public func height(_ value: CGFloat) -> CGFloat { - return value * heightScale - } + public func height(_ value: CGFloat) -> CGFloat { cache.scaleHeight(value) } /// Returns `value` scaled by the captured text factor. @inline(__always) - public func text(_ value: CGFloat) -> CGFloat { - return value * textScale - } + public func text(_ value: CGFloat) -> CGFloat { cache.scaleText(value) } /// Returns `value` scaled by the smaller of the captured width and height factors. @inline(__always) - public func radius(_ value: CGFloat) -> CGFloat { - return value * min(widthScale, heightScale) - } + public func radius(_ value: CGFloat) -> CGFloat { cache.scaleRadius(value) } /// Returns `value` with width and height each scaled by their respective captured factors. @inline(__always) public func size(_ value: CGSize) -> CGSize { - return CGSize(width: value.width * widthScale, height: value.height * heightScale) + CGSize(width: cache.scaleWidth(value.width), height: cache.scaleHeight(value.height)) } /// Returns `value` with x scaled by the width factor and y by the height factor. @inline(__always) public func point(_ value: CGPoint) -> CGPoint { - return CGPoint(x: value.x * widthScale, y: value.y * heightScale) + CGPoint(x: cache.scaleWidth(value.x), y: cache.scaleHeight(value.y)) } /// Returns `value` with origin and size each scaled by their respective captured factors. @inline(__always) public func rect(_ value: CGRect) -> CGRect { - return CGRect( - x: value.origin.x * widthScale, - y: value.origin.y * heightScale, - width: value.size.width * widthScale, - height: value.size.height * heightScale + CGRect( + x: cache.scaleWidth(value.origin.x), + y: cache.scaleHeight(value.origin.y), + width: cache.scaleWidth(value.size.width), + height: cache.scaleHeight(value.size.height) ) } } @@ -73,8 +61,7 @@ public extension ScreenUtil { /// A `FastScale` instance pre-loaded with the current scale factors; use this to avoid per-value singleton lookups. @inline(__always) var fastScale: FastScale { - let s = _snapshot - return FastScale(widthScale: s.scaleWidth, heightScale: s.scaleHeight, textScale: s.scaleText) + FastScale(cache: ScaleFactorCache(from: self)) } } diff --git a/Sources/ScreenUtil/Scaling/Numeric+Scaling.swift b/Sources/ScreenUtil/Scaling/Numeric+Scaling.swift index 22e040c..4b1142e 100644 --- a/Sources/ScreenUtil/Scaling/Numeric+Scaling.swift +++ b/Sources/ScreenUtil/Scaling/Numeric+Scaling.swift @@ -9,226 +9,35 @@ import Foundation import CoreGraphics -public extension Int { - /// Value scaled by the width factor (design width → device width). - @inline(__always) - var w: CGFloat { - return ScreenUtil.shared.w(CGFloat(self)) - } - - /// Value scaled by the height factor (design height → device height). - @inline(__always) - var h: CGFloat { - return ScreenUtil.shared.h(CGFloat(self)) - } - - /// Value scaled as a font size (uses min of width/height when `minTextAdapt` is enabled). - @inline(__always) - var sp: CGFloat { - return ScreenUtil.shared.sp(CGFloat(self)) - } - - /// Value scaled by the smaller of width/height factors, suitable for corner radii. - @inline(__always) - var r: CGFloat { - return ScreenUtil.shared.r(CGFloat(self)) - } - - /// Value expressed as a fraction of the screen width (0…1 → 0…screenWidth). - @inline(__always) - var sw: CGFloat { - return ScreenUtil.shared.sw(CGFloat(self)) - } - - /// Value expressed as a fraction of the screen height (0…1 → 0…screenHeight). - @inline(__always) - var sh: CGFloat { - return ScreenUtil.shared.sh(CGFloat(self)) - } - - /// Width-scaled value using the cached fast path (lower overhead than `.w` for tight loops). - @inline(__always) - var fastW: CGFloat { - return ScreenUtil.shared.fastW(CGFloat(self)) - } - - /// Height-scaled value using the cached fast path (lower overhead than `.h` for tight loops). - @inline(__always) - var fastH: CGFloat { - return ScreenUtil.shared.fastH(CGFloat(self)) - } +// Scaling extensions for every integer and floating-point numeric type. +// `cgFloatValue` is the single Numeric→CGFloat bridge (Int64/UInt/CGFloat safe). - /// Font-size-scaled value using the cached fast path (lower overhead than `.sp` for tight loops). - @inline(__always) - var fastSp: CGFloat { - return ScreenUtil.shared.fastSp(CGFloat(self)) - } -} - -public extension Float { +public extension BinaryInteger { /// Value scaled by the width factor (design width → device width). - @inline(__always) - var w: CGFloat { - return ScreenUtil.shared.w(CGFloat(self)) - } - + @inline(__always) var w: CGFloat { ScreenUtil.shared.w(cgFloatValue(self)) } /// Value scaled by the height factor (design height → device height). - @inline(__always) - var h: CGFloat { - return ScreenUtil.shared.h(CGFloat(self)) - } - - /// Value scaled as a font size (uses min of width/height when `minTextAdapt` is enabled). - @inline(__always) - var sp: CGFloat { - return ScreenUtil.shared.sp(CGFloat(self)) - } - + @inline(__always) var h: CGFloat { ScreenUtil.shared.h(cgFloatValue(self)) } + /// Value scaled as a font size (min of width/height when `minTextAdapt` is enabled). + @inline(__always) var sp: CGFloat { ScreenUtil.shared.sp(cgFloatValue(self)) } /// Value scaled by the smaller of width/height factors, suitable for corner radii. - @inline(__always) - var r: CGFloat { - return ScreenUtil.shared.r(CGFloat(self)) - } - + @inline(__always) var r: CGFloat { ScreenUtil.shared.r(cgFloatValue(self)) } /// Value expressed as a fraction of the screen width (0…1 → 0…screenWidth). - @inline(__always) - var sw: CGFloat { - return ScreenUtil.shared.sw(CGFloat(self)) - } - + @inline(__always) var sw: CGFloat { ScreenUtil.shared.sw(cgFloatValue(self)) } /// Value expressed as a fraction of the screen height (0…1 → 0…screenHeight). - @inline(__always) - var sh: CGFloat { - return ScreenUtil.shared.sh(CGFloat(self)) - } - - /// Width-scaled value using the cached fast path (lower overhead than `.w` for tight loops). - @inline(__always) - var fastW: CGFloat { - return ScreenUtil.shared.fastW(CGFloat(self)) - } - - /// Height-scaled value using the cached fast path (lower overhead than `.h` for tight loops). - @inline(__always) - var fastH: CGFloat { - return ScreenUtil.shared.fastH(CGFloat(self)) - } - - /// Font-size-scaled value using the cached fast path (lower overhead than `.sp` for tight loops). - @inline(__always) - var fastSp: CGFloat { - return ScreenUtil.shared.fastSp(CGFloat(self)) - } + @inline(__always) var sh: CGFloat { ScreenUtil.shared.sh(cgFloatValue(self)) } } -public extension Double { +public extension BinaryFloatingPoint { /// Value scaled by the width factor (design width → device width). - @inline(__always) - var w: CGFloat { - return ScreenUtil.shared.w(CGFloat(self)) - } - + @inline(__always) var w: CGFloat { ScreenUtil.shared.w(cgFloatValue(self)) } /// Value scaled by the height factor (design height → device height). - @inline(__always) - var h: CGFloat { - return ScreenUtil.shared.h(CGFloat(self)) - } - - /// Value scaled as a font size (uses min of width/height when `minTextAdapt` is enabled). - @inline(__always) - var sp: CGFloat { - return ScreenUtil.shared.sp(CGFloat(self)) - } - - /// Value scaled by the smaller of width/height factors, suitable for corner radii. - @inline(__always) - var r: CGFloat { - return ScreenUtil.shared.r(CGFloat(self)) - } - - /// Value expressed as a fraction of the screen width (0…1 → 0…screenWidth). - @inline(__always) - var sw: CGFloat { - return ScreenUtil.shared.sw(CGFloat(self)) - } - - /// Value expressed as a fraction of the screen height (0…1 → 0…screenHeight). - @inline(__always) - var sh: CGFloat { - return ScreenUtil.shared.sh(CGFloat(self)) - } - - /// Width-scaled value using the cached fast path (lower overhead than `.w` for tight loops). - @inline(__always) - var fastW: CGFloat { - return ScreenUtil.shared.fastW(CGFloat(self)) - } - - /// Height-scaled value using the cached fast path (lower overhead than `.h` for tight loops). - @inline(__always) - var fastH: CGFloat { - return ScreenUtil.shared.fastH(CGFloat(self)) - } - - /// Font-size-scaled value using the cached fast path (lower overhead than `.sp` for tight loops). - @inline(__always) - var fastSp: CGFloat { - return ScreenUtil.shared.fastSp(CGFloat(self)) - } -} - -public extension CGFloat { - /// Value scaled by the width factor (design width → device width). - @inline(__always) - var w: CGFloat { - return ScreenUtil.shared.w(self) - } - - /// Value scaled by the height factor (design height → device height). - @inline(__always) - var h: CGFloat { - return ScreenUtil.shared.h(self) - } - - /// Value scaled as a font size (uses min of width/height when `minTextAdapt` is enabled). - @inline(__always) - var sp: CGFloat { - return ScreenUtil.shared.sp(self) - } - + @inline(__always) var h: CGFloat { ScreenUtil.shared.h(cgFloatValue(self)) } + /// Value scaled as a font size (min of width/height when `minTextAdapt` is enabled). + @inline(__always) var sp: CGFloat { ScreenUtil.shared.sp(cgFloatValue(self)) } /// Value scaled by the smaller of width/height factors, suitable for corner radii. - @inline(__always) - var r: CGFloat { - return ScreenUtil.shared.r(self) - } - + @inline(__always) var r: CGFloat { ScreenUtil.shared.r(cgFloatValue(self)) } /// Value expressed as a fraction of the screen width (0…1 → 0…screenWidth). - @inline(__always) - var sw: CGFloat { - return ScreenUtil.shared.sw(self) - } - + @inline(__always) var sw: CGFloat { ScreenUtil.shared.sw(cgFloatValue(self)) } /// Value expressed as a fraction of the screen height (0…1 → 0…screenHeight). - @inline(__always) - var sh: CGFloat { - return ScreenUtil.shared.sh(self) - } - - /// Width-scaled value using the cached fast path (lower overhead than `.w` for tight loops). - @inline(__always) - var fastW: CGFloat { - return ScreenUtil.shared.fastW(self) - } - - /// Height-scaled value using the cached fast path (lower overhead than `.h` for tight loops). - @inline(__always) - var fastH: CGFloat { - return ScreenUtil.shared.fastH(self) - } - - /// Font-size-scaled value using the cached fast path (lower overhead than `.sp` for tight loops). - @inline(__always) - var fastSp: CGFloat { - return ScreenUtil.shared.fastSp(self) - } + @inline(__always) var sh: CGFloat { ScreenUtil.shared.sh(cgFloatValue(self)) } } diff --git a/Sources/ScreenUtil/SwiftUI/Environment+ScreenUtil.swift b/Sources/ScreenUtil/SwiftUI/Environment+ScreenUtil.swift new file mode 100644 index 0000000..737fc32 --- /dev/null +++ b/Sources/ScreenUtil/SwiftUI/Environment+ScreenUtil.swift @@ -0,0 +1,24 @@ +// +// Environment+ScreenUtil.swift +// ScreenUtil +// +// SwiftUI environment access to the shared ScreenUtil instance +// Created by Dicky Darmawan on 06/06/26. +// + +#if canImport(SwiftUI) +import SwiftUI + +private struct ScreenUtilKey: EnvironmentKey { + static let defaultValue: ScreenUtil = .shared +} + +public extension EnvironmentValues { + /// Access ScreenUtil instance from environment. + var screenUtil: ScreenUtil { + get { self[ScreenUtilKey.self] } + set { self[ScreenUtilKey.self] = newValue } + } +} + +#endif diff --git a/Sources/ScreenUtil/SwiftUI/ScaledValue.swift b/Sources/ScreenUtil/SwiftUI/ScaledValue.swift deleted file mode 100644 index 7b26f49..0000000 --- a/Sources/ScreenUtil/SwiftUI/ScaledValue.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// ScaledValue.swift -// ScreenUtil -// -// SwiftUI property wrappers for responsive values -// Created by Dicky Darmawan on 06/06/26. -// - -#if canImport(SwiftUI) -import SwiftUI - -/// Property wrapper for a design value scaled to the device. -@propertyWrapper -public struct ScaledValue { - private let value: CGFloat - private let type: ScaleType - - public init(wrappedValue: CGFloat, _ type: ScaleType = .width) { - self.value = wrappedValue - self.type = type - } - - public var wrappedValue: CGFloat { - switch type { - case .width: return value.w - case .height: return value.h - case .text: return value.sp - case .radius: return value.r - } - } -} - -/// Property wrapper for a percentage of a screen dimension. -@propertyWrapper -public struct ScreenPercentage { - private let percentage: CGFloat - private let dimension: Dimension - - public enum Dimension { - case width - case height - } - - public init(wrappedValue: CGFloat, _ dimension: Dimension) { - self.percentage = wrappedValue - self.dimension = dimension - } - - public var wrappedValue: CGFloat { - switch dimension { - case .width: - return percentage.sw - case .height: - return percentage.sh - } - } -} - -#endif diff --git a/Sources/ScreenUtil/SwiftUI/View+Responsive.swift b/Sources/ScreenUtil/SwiftUI/View+Responsive.swift deleted file mode 100644 index 5d9dc3f..0000000 --- a/Sources/ScreenUtil/SwiftUI/View+Responsive.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// View+Responsive.swift -// ScreenUtil -// -// SwiftUI view, font, and environment support -// Created by Dicky Darmawan on 06/06/26. -// - -#if canImport(SwiftUI) -import SwiftUI - -// MARK: - View Modifiers - -public extension View { - /// Apply a responsive frame (design values scaled to the device). - func responsiveFrame( - width: CGFloat? = nil, - height: CGFloat? = nil, - alignment: Alignment = .center - ) -> some View { - self.frame(width: width?.w, height: height?.h, alignment: alignment) - } - - /// Apply responsive padding to an edge set. - func responsivePadding(_ edges: Edge.Set = .all, _ length: CGFloat) -> some View { - self.padding(edges, edges.isHorizontal ? length.w : length.h) - } - - /// Apply responsive padding from design insets. - func responsivePadding(_ insets: EdgeInsets) -> some View { - self.padding(EdgeInsets( - top: insets.top.h, - leading: insets.leading.w, - bottom: insets.bottom.h, - trailing: insets.trailing.w - )) - } - - /// Apply a scaled corner radius. - func responsiveCornerRadius(_ radius: CGFloat) -> some View { - self.clipShape(RoundedRectangle(cornerRadius: radius.r, style: .continuous)) - } -} - -// MARK: - Font - -public extension Font { - /// Create a system font with scaled size. - static func scaledSystem( - size: CGFloat, - weight: Font.Weight = .regular, - design: Font.Design = .default - ) -> Font { - return .system(size: size.sp, weight: weight, design: design) - } - - /// Create a custom font with scaled size. - static func scaledCustom(_ name: String, size: CGFloat) -> Font { - return .custom(name, size: size.sp) - } -} - -// MARK: - Environment - -private struct ScreenUtilKey: EnvironmentKey { - static let defaultValue: ScreenUtil = .shared -} - -public extension EnvironmentValues { - /// Access ScreenUtil instance from environment. - var screenUtil: ScreenUtil { - get { self[ScreenUtilKey.self] } - set { self[ScreenUtilKey.self] = newValue } - } -} - -// MARK: - Helpers - -private extension Edge.Set { - var isHorizontal: Bool { - return self == .horizontal || self == .leading || self == .trailing - } -} - -#endif diff --git a/Sources/ScreenUtil/UIKit/UIView+Scaling.swift b/Sources/ScreenUtil/UIKit/UIView+Scaling.swift index a712e3b..c675448 100644 --- a/Sources/ScreenUtil/UIKit/UIView+Scaling.swift +++ b/Sources/ScreenUtil/UIKit/UIView+Scaling.swift @@ -13,25 +13,28 @@ public extension UIView { /// Apply width constraint with scaling @discardableResult func width(_ constant: CGFloat) -> NSLayoutConstraint { - let constraint = self.widthAnchor.constraint(equalToConstant: constant.w) - constraint.isActive = true + translatesAutoresizingMaskIntoConstraints = false + let constraint = widthAnchor.constraint(equalToConstant: constant.w) + NSLayoutConstraint.activate([constraint]) return constraint } /// Apply height constraint with scaling @discardableResult func height(_ constant: CGFloat) -> NSLayoutConstraint { - let constraint = self.heightAnchor.constraint(equalToConstant: constant.h) - constraint.isActive = true + translatesAutoresizingMaskIntoConstraints = false + let constraint = heightAnchor.constraint(equalToConstant: constant.h) + NSLayoutConstraint.activate([constraint]) return constraint } /// Apply size constraints with scaling @discardableResult func size(width: CGFloat, height: CGFloat) -> [NSLayoutConstraint] { + translatesAutoresizingMaskIntoConstraints = false let constraints = [ - self.widthAnchor.constraint(equalToConstant: width.w), - self.heightAnchor.constraint(equalToConstant: height.h) + widthAnchor.constraint(equalToConstant: width.w), + heightAnchor.constraint(equalToConstant: height.h) ] NSLayoutConstraint.activate(constraints) return constraints diff --git a/Tests/ScreenUtilTests/Core/ScreenUtilTests.swift b/Tests/ScreenUtilTests/Core/ScreenUtilTests.swift index 5390bde..5c36f63 100644 --- a/Tests/ScreenUtilTests/Core/ScreenUtilTests.swift +++ b/Tests/ScreenUtilTests/Core/ScreenUtilTests.swift @@ -69,22 +69,6 @@ final class ScreenUtilTests: XCTestCase { XCTAssertEqual(textResult, testValue * screenUtil.scaleText, accuracy: 0.01) } - func testFastScalingOperations() { - let testValue: CGFloat = 50.0 - - let fastWidthResult = screenUtil.fastW(testValue) - let fastHeightResult = screenUtil.fastH(testValue) - let fastTextResult = screenUtil.fastSp(testValue) - - let normalWidthResult = screenUtil.w(testValue) - let normalHeightResult = screenUtil.h(testValue) - let normalTextResult = screenUtil.sp(testValue) - - XCTAssertEqual(fastWidthResult, normalWidthResult, accuracy: 0.01) - XCTAssertEqual(fastHeightResult, normalHeightResult, accuracy: 0.01) - XCTAssertEqual(fastTextResult, normalTextResult, accuracy: 0.01) - } - func testScreenPercentages() { let screenWidth = screenUtil.screenWidth let screenHeight = screenUtil.screenHeight @@ -180,9 +164,9 @@ final class ScreenUtilTests: XCTestCase { func testBatchOperations() { let testValues = [10, 20, 30, 40, 50] - let batchWidths = screenUtil.batchWidths(testValues) - let batchHeights = screenUtil.batchHeights(testValues) - let batchFontSizes = screenUtil.batchFontSizes(testValues) + let batchWidths = screenUtil.batchScaler.widths(testValues) + let batchHeights = screenUtil.batchScaler.heights(testValues) + let batchFontSizes = screenUtil.batchScaler.fontSizes(testValues) XCTAssertEqual(batchWidths.count, testValues.count) XCTAssertEqual(batchHeights.count, testValues.count) diff --git a/Tests/ScreenUtilTests/Debug/DebugTests.swift b/Tests/ScreenUtilTests/Debug/DebugTests.swift index f66efb8..295e185 100644 --- a/Tests/ScreenUtilTests/Debug/DebugTests.swift +++ b/Tests/ScreenUtilTests/Debug/DebugTests.swift @@ -14,10 +14,8 @@ final class DebugTests: XCTestCase { func testDebugUtilitiesRun() { let su = ScreenUtil.shared su.configure(with: .iPhone13Pro) - su.debug.printCurrentConfiguration() - XCTAssertTrue(su.debug.validateScaling()) - XCTAssertFalse(su.debug.generateTestReport().isEmpty) - let (result, avg) = su.debug.measurePerformance({ 10.w }, iterations: 100) + ScreenUtilDebug.printCurrentConfiguration() + let (result, avg) = ScreenUtilDebug.measurePerformance({ 10.w }, iterations: 100) XCTAssertGreaterThan(result, 0) XCTAssertGreaterThanOrEqual(avg, 0) } @@ -28,6 +26,6 @@ final class DebugTests: XCTestCase { ScreenUtil.shared.configure(with: ScreenUtilConfiguration( designSize: CGSize(width: 100, height: 100), scalingLimits: ScalingLimits(minScale: 0.1, maxScale: 10))) - ScreenUtil.shared.debug.benchmarkScalingOperations() + ScreenUtilDebug.benchmarkScalingOperations() } } diff --git a/Tests/ScreenUtilTests/Performance/PerformanceTests.swift b/Tests/ScreenUtilTests/Performance/PerformanceTests.swift index b114fab..3bb33a6 100644 --- a/Tests/ScreenUtilTests/Performance/PerformanceTests.swift +++ b/Tests/ScreenUtilTests/Performance/PerformanceTests.swift @@ -27,22 +27,12 @@ final class PerformanceTests: XCTestCase { } } - func testFastScalingPerformance() { - let testValue: CGFloat = 100.0 - - measure { - for _ in 0..<10000 { - _ = testValue.fastW - } - } - } - func testBatchOperationsPerformance() { let testValues = Array(1...100).map { CGFloat($0) } measure { for _ in 0..<100 { - _ = screenUtil.batchWidths(testValues) + _ = screenUtil.batchScaler.widths(testValues) } } } @@ -137,27 +127,6 @@ final class PerformanceTests: XCTestCase { } } - func testPerformanceComparison() { - let iterations = 50000 - let testValue: CGFloat = 100.0 - - let standardTime = measureTime { - for _ in 0.. Void) -> TimeInterval { let startTime = CFAbsoluteTimeGetCurrent() block() diff --git a/Tests/ScreenUtilTests/Scaling/BatchScalingTests.swift b/Tests/ScreenUtilTests/Scaling/BatchScalingTests.swift index 6dd7c1b..7b9ae22 100644 --- a/Tests/ScreenUtilTests/Scaling/BatchScalingTests.swift +++ b/Tests/ScreenUtilTests/Scaling/BatchScalingTests.swift @@ -26,7 +26,7 @@ final class BatchScalingTests: XCTestCase { func testBatchScaleWidth() { let su = ScreenUtil.shared let input: [CGFloat] = [10, 20, 30] - let out = su.batchScale(input, scaleType: .width) + let out = su.batchScaler.scale(input, scaleType: .width) XCTAssertEqual(out.count, 3) XCTAssertEqual(out[0], su.w(10), accuracy: 0.001) XCTAssertEqual(out[1], su.w(20), accuracy: 0.001) @@ -37,7 +37,7 @@ final class BatchScalingTests: XCTestCase { func testBatchScaleHeight() { let su = ScreenUtil.shared let input: [CGFloat] = [5, 15, 25] - let out = su.batchScale(input, scaleType: .height) + let out = su.batchScaler.scale(input, scaleType: .height) XCTAssertEqual(out.count, 3) XCTAssertTrue(out.allSatisfy { $0.isFinite && $0 > 0 }) XCTAssertEqual(out[0], su.h(5), accuracy: 0.001) @@ -46,7 +46,7 @@ final class BatchScalingTests: XCTestCase { func testBatchScaleText() { let su = ScreenUtil.shared let input: [CGFloat] = [12, 14, 16, 18] - let out = su.batchScale(input, scaleType: .text) + let out = su.batchScaler.scale(input, scaleType: .text) XCTAssertEqual(out.count, 4) XCTAssertTrue(out.allSatisfy { $0.isFinite && $0 > 0 }) XCTAssertEqual(out[0], su.sp(12), accuracy: 0.001) @@ -55,7 +55,7 @@ final class BatchScalingTests: XCTestCase { func testBatchScaleRadius() { let su = ScreenUtil.shared let input: [CGFloat] = [4, 8, 12] - let out = su.batchScale(input, scaleType: .radius) + let out = su.batchScaler.scale(input, scaleType: .radius) XCTAssertEqual(out.count, 3) XCTAssertTrue(out.allSatisfy { $0.isFinite && $0 > 0 }) XCTAssertEqual(out[0], su.r(4), accuracy: 0.001) @@ -65,34 +65,34 @@ final class BatchScalingTests: XCTestCase { func testBatchWidthsMixedNumericTypes() { let su = ScreenUtil.shared - let intOut = su.batchWidths([Int(10), 20, 30]) + let intOut = su.batchScaler.widths([Int(10), 20, 30]) XCTAssertEqual(intOut.count, 3) XCTAssertTrue(intOut.allSatisfy { $0.isFinite && $0 > 0 }) - let cgOut = su.batchWidths([CGFloat(5), 15]) + let cgOut = su.batchScaler.widths([CGFloat(5), 15]) XCTAssertTrue(cgOut.allSatisfy { $0.isFinite && $0 > 0 }) - let int64Out = su.batchWidths([Int64(100), 200]) + let int64Out = su.batchScaler.widths([Int64(100), 200]) XCTAssertTrue(int64Out.allSatisfy { $0.isFinite && $0 > 0 }) } func testBatchHeightsMixedNumericTypes() { let su = ScreenUtil.shared - let out = su.batchHeights([Int(50), 60, 70]) + let out = su.batchScaler.heights([Int(50), 60, 70]) XCTAssertEqual(out.count, 3) XCTAssertTrue(out.allSatisfy { $0 > 0 }) - let floatOut = su.batchHeights([Float(10), 20]) + let floatOut = su.batchScaler.heights([Float(10), 20]) XCTAssertTrue(floatOut.allSatisfy { $0 > 0 }) } func testBatchFontSizesMixedTypes() { let su = ScreenUtil.shared - let out = su.batchFontSizes([CGFloat(12), 14, 16]) + let out = su.batchScaler.fontSizes([CGFloat(12), 14, 16]) XCTAssertEqual(out.count, 3) XCTAssertTrue(out.allSatisfy { $0.isFinite && $0 > 0 }) - let intOut = su.batchFontSizes([Int(10), 12]) + let intOut = su.batchScaler.fontSizes([Int(10), 12]) XCTAssertTrue(intOut.allSatisfy { $0 > 0 }) } @@ -101,7 +101,7 @@ final class BatchScalingTests: XCTestCase { func testBatchSizes() { let su = ScreenUtil.shared let sizes = [CGSize(width: 10, height: 20), CGSize(width: 30, height: 40)] - let out = su.batchSizes(sizes) + let out = su.batchScaler.sizes(sizes) XCTAssertEqual(out.count, 2) XCTAssertEqual(out[0].width, su.w(10), accuracy: 0.001) XCTAssertEqual(out[0].height, su.h(20), accuracy: 0.001) @@ -110,7 +110,7 @@ final class BatchScalingTests: XCTestCase { } func testBatchSizesEmpty() { - let out = ScreenUtil.shared.batchSizes([]) + let out = ScreenUtil.shared.batchScaler.sizes([]) XCTAssertEqual(out.count, 0) } @@ -119,7 +119,7 @@ final class BatchScalingTests: XCTestCase { func testBatchPoints() { let su = ScreenUtil.shared let points = [CGPoint(x: 5, y: 10), CGPoint(x: 15, y: 25)] - let out = su.batchPoints(points) + let out = su.batchScaler.points(points) XCTAssertEqual(out.count, 2) XCTAssertEqual(out[0].x, su.w(5), accuracy: 0.001) XCTAssertEqual(out[0].y, su.h(10), accuracy: 0.001) @@ -134,7 +134,7 @@ final class BatchScalingTests: XCTestCase { CGRect(x: 1, y: 2, width: 10, height: 20), CGRect(x: 3, y: 4, width: 30, height: 40) ] - let out = su.batchRects(rects) + let out = su.batchScaler.rects(rects) XCTAssertEqual(out.count, 2) XCTAssertEqual(out[0].origin.x, su.w(1), accuracy: 0.001) XCTAssertEqual(out[0].origin.y, su.h(2), accuracy: 0.001) diff --git a/Tests/ScreenUtilTests/Scaling/FastBatchTests.swift b/Tests/ScreenUtilTests/Scaling/FastBatchTests.swift index 39aa42c..8f79620 100644 --- a/Tests/ScreenUtilTests/Scaling/FastBatchTests.swift +++ b/Tests/ScreenUtilTests/Scaling/FastBatchTests.swift @@ -28,10 +28,10 @@ final class FastBatchTests: XCTestCase { func testBatchNumericTypesNeverZero() { let su = ScreenUtil.shared - XCTAssertTrue(su.batchWidths([CGFloat(10), 20, 30]).allSatisfy { $0 > 0 }) - XCTAssertTrue(su.batchWidths([Int64(10), 20, 30]).allSatisfy { $0 > 0 }) - XCTAssertTrue(su.batchHeights([10, 20]).allSatisfy { $0 > 0 }) - XCTAssertTrue(su.batchFontSizes([Float(12), 14]).allSatisfy { $0 > 0 }) + XCTAssertTrue(su.batchScaler.widths([CGFloat(10), 20, 30]).allSatisfy { $0 > 0 }) + XCTAssertTrue(su.batchScaler.widths([Int64(10), 20, 30]).allSatisfy { $0 > 0 }) + XCTAssertTrue(su.batchScaler.heights([10, 20]).allSatisfy { $0 > 0 }) + XCTAssertTrue(su.batchScaler.fontSizes([Float(12), 14]).allSatisfy { $0 > 0 }) } func testBatchScalerWidths() { @@ -43,7 +43,7 @@ final class FastBatchTests: XCTestCase { func testBatchScaleAllTypes() { let su = ScreenUtil.shared for t: ScaleType in [.width, .height, .text, .radius] { - XCTAssertTrue(su.batchScale([10, 20], scaleType: t).allSatisfy { $0 > 0 }) + XCTAssertTrue(su.batchScaler.scale([10, 20], scaleType: t).allSatisfy { $0 > 0 }) } } } diff --git a/Tests/ScreenUtilTests/Scaling/FastScaleExtendedTests.swift b/Tests/ScreenUtilTests/Scaling/FastScaleExtendedTests.swift index 1739269..bbb9192 100644 --- a/Tests/ScreenUtilTests/Scaling/FastScaleExtendedTests.swift +++ b/Tests/ScreenUtilTests/Scaling/FastScaleExtendedTests.swift @@ -83,9 +83,9 @@ final class FastScaleExtendedTests: XCTestCase { func testFastScaleWidthHeightTextRadius() { let fs = ScreenUtil.shared.fastScale let su = ScreenUtil.shared - XCTAssertEqual(fs.width(100), su.fastW(100), accuracy: 0.001) - XCTAssertEqual(fs.height(100), su.fastH(100), accuracy: 0.001) - XCTAssertEqual(fs.text(100), su.fastSp(100), accuracy: 0.001) + XCTAssertEqual(fs.width(100), su.w(100), accuracy: 0.001) + XCTAssertEqual(fs.height(100), su.h(100), accuracy: 0.001) + XCTAssertEqual(fs.text(100), su.sp(100), accuracy: 0.001) XCTAssertGreaterThan(fs.radius(100), 0) } diff --git a/Tests/ScreenUtilTests/Scaling/NumericScalingTests.swift b/Tests/ScreenUtilTests/Scaling/NumericScalingTests.swift index 084f8e5..26face6 100644 --- a/Tests/ScreenUtilTests/Scaling/NumericScalingTests.swift +++ b/Tests/ScreenUtilTests/Scaling/NumericScalingTests.swift @@ -24,7 +24,7 @@ final class NumericScalingTests: XCTestCase { func testIntAllProperties() { let v: Int = 50 - let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh, v.fastW, v.fastH, v.fastSp] + let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh] for (i, p) in props.enumerated() { XCTAssertTrue(p.isFinite, "Int property[\(i)] not finite") XCTAssertGreaterThan(p, 0, "Int property[\(i)] not > 0") @@ -40,16 +40,13 @@ final class NumericScalingTests: XCTestCase { XCTAssertEqual(v.r, su.r(CGFloat(v)), accuracy: 0.001) XCTAssertEqual(v.sw, su.sw(CGFloat(v)), accuracy: 0.001) XCTAssertEqual(v.sh, su.sh(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastW, su.fastW(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastH, su.fastH(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastSp, su.fastSp(CGFloat(v)), accuracy: 0.001) } // MARK: - Float func testFloatAllProperties() { let v: Float = 50.0 - let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh, v.fastW, v.fastH, v.fastSp] + let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh] for (i, p) in props.enumerated() { XCTAssertTrue(p.isFinite, "Float property[\(i)] not finite") XCTAssertGreaterThan(p, 0, "Float property[\(i)] not > 0") @@ -63,16 +60,13 @@ final class NumericScalingTests: XCTestCase { XCTAssertEqual(v.h, su.h(CGFloat(v)), accuracy: 0.001) XCTAssertEqual(v.sp, su.sp(CGFloat(v)), accuracy: 0.001) XCTAssertEqual(v.r, su.r(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastW, su.fastW(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastH, su.fastH(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastSp, su.fastSp(CGFloat(v)), accuracy: 0.001) } // MARK: - Double func testDoubleAllProperties() { let v: Double = 50.0 - let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh, v.fastW, v.fastH, v.fastSp] + let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh] for (i, p) in props.enumerated() { XCTAssertTrue(p.isFinite, "Double property[\(i)] not finite") XCTAssertGreaterThan(p, 0, "Double property[\(i)] not > 0") @@ -86,16 +80,13 @@ final class NumericScalingTests: XCTestCase { XCTAssertEqual(v.h, su.h(CGFloat(v)), accuracy: 0.001) XCTAssertEqual(v.sp, su.sp(CGFloat(v)), accuracy: 0.001) XCTAssertEqual(v.r, su.r(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastW, su.fastW(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastH, su.fastH(CGFloat(v)), accuracy: 0.001) - XCTAssertEqual(v.fastSp, su.fastSp(CGFloat(v)), accuracy: 0.001) } // MARK: - CGFloat func testCGFloatAllProperties() { let v: CGFloat = 50.0 - let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh, v.fastW, v.fastH, v.fastSp] + let props: [CGFloat] = [v.w, v.h, v.sp, v.r, v.sw, v.sh] for (i, p) in props.enumerated() { XCTAssertTrue(p.isFinite, "CGFloat property[\(i)] not finite") XCTAssertGreaterThan(p, 0, "CGFloat property[\(i)] not > 0") @@ -111,15 +102,11 @@ final class NumericScalingTests: XCTestCase { XCTAssertEqual(v.r, su.r(v), accuracy: 0.001) XCTAssertEqual(v.sw, su.sw(v), accuracy: 0.001) XCTAssertEqual(v.sh, su.sh(v), accuracy: 0.001) - XCTAssertEqual(v.fastW, su.fastW(v), accuracy: 0.001) - XCTAssertEqual(v.fastH, su.fastH(v), accuracy: 0.001) - XCTAssertEqual(v.fastSp, su.fastSp(v), accuracy: 0.001) } // MARK: - Proportional correctness func testDoubleScalesProportionally() { - let su = ScreenUtil.shared let a: Double = 10.0 let b: Double = 20.0 XCTAssertEqual(b.w / a.w, 2.0, accuracy: 0.001, "scaling must be linear") diff --git a/Tests/ScreenUtilTests/SwiftUI/PropertyWrapperTests.swift b/Tests/ScreenUtilTests/SwiftUI/PropertyWrapperTests.swift deleted file mode 100644 index d0c0569..0000000 --- a/Tests/ScreenUtilTests/SwiftUI/PropertyWrapperTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// PropertyWrapperTests.swift -// ScreenUtilTests -// -// @ScaledValue / @ScreenPercentage resolution. -// Created by Dicky Darmawan on 06/06/26. -// - -#if canImport(SwiftUI) -import XCTest -@testable import ScreenUtil - -final class PropertyWrapperTests: XCTestCase { - @MainActor - func testScaledValueResolves() { - let su = ScreenUtil.shared - su.configure(with: ScreenUtilConfiguration(designSize: CGSize(width: 100, height: 100), - scalingLimits: ScalingLimits(minScale: 0.1, maxScale: 10))) - let w = ScaledValue(wrappedValue: 10, .width) - XCTAssertEqual(w.wrappedValue, 10 * su.scaleWidth, accuracy: 0.001) - let f = ScaledValue(wrappedValue: 16, .text) - XCTAssertEqual(f.wrappedValue, 16 * su.scaleText, accuracy: 0.001) - } - - @MainActor - func testScreenPercentageResolves() { - let su = ScreenUtil.shared - su.configure(with: ScreenUtilConfiguration(designSize: CGSize(width: 100, height: 100))) - let p = ScreenPercentage(wrappedValue: 50, .width) - XCTAssertEqual(p.wrappedValue, su.screenWidth * 0.5, accuracy: 0.001) - } - - @MainActor - func testScaledValueHeightRadius() { - let su = ScreenUtil.shared - su.configure(with: ScreenUtilConfiguration(designSize: CGSize(width: 100, height: 100), - scalingLimits: ScalingLimits(minScale: 0.1, maxScale: 10))) - let h = ScaledValue(wrappedValue: 20, .height) - XCTAssertEqual(h.wrappedValue, 20 * su.scaleHeight, accuracy: 0.001) - - let r = ScaledValue(wrappedValue: 8, .radius) - XCTAssertEqual(r.wrappedValue, su.r(8), accuracy: 0.001) - } - - @MainActor - func testScreenPercentageHeight() { - let su = ScreenUtil.shared - su.configure(with: ScreenUtilConfiguration(designSize: CGSize(width: 100, height: 100))) - let p = ScreenPercentage(wrappedValue: 25, .height) - XCTAssertEqual(p.wrappedValue, su.screenHeight * 0.25, accuracy: 0.001) - } -} -#endif diff --git a/Tests/ScreenUtilTests/SwiftUI/SwiftUIFontEnvironmentTests.swift b/Tests/ScreenUtilTests/SwiftUI/SwiftUIFontEnvironmentTests.swift index 5bb6e01..649e678 100644 --- a/Tests/ScreenUtilTests/SwiftUI/SwiftUIFontEnvironmentTests.swift +++ b/Tests/ScreenUtilTests/SwiftUI/SwiftUIFontEnvironmentTests.swift @@ -2,8 +2,7 @@ // SwiftUIFontEnvironmentTests.swift // ScreenUtilTests // -// Font.scaledSystem / Font.scaledCustom / EnvironmentValues.screenUtil coverage. -// View modifier rendering is skipped (requires a host); Font + Environment are testable. +// EnvironmentValues.screenUtil coverage. Font/wrapper sugar removed; View modifier rendering needs a host. // Created by Dicky Darmawan on 06/06/26. // @@ -24,47 +23,6 @@ final class SwiftUIFontEnvironmentTests: XCTestCase { } } - // MARK: - Font.scaledSystem - - func testScaledSystemFontReturnsValue() { - // Font is opaque — just verify the call doesn't crash and returns. - let font = Font.scaledSystem(size: 16) - _ = font // used - } - - func testScaledSystemFontWithWeightAndDesign() { - let font = Font.scaledSystem(size: 14, weight: .bold, design: .rounded) - _ = font - } - - func testScaledSystemFontZeroSize() { - // Should not crash even with zero size. - let font = Font.scaledSystem(size: 0) - _ = font - } - - func testScaledSystemFontVariousWeights() { - let weights: [Font.Weight] = [.ultraLight, .thin, .light, .regular, .medium, .semibold, .bold, .heavy, .black] - for w in weights { - let font = Font.scaledSystem(size: 12, weight: w) - _ = font - } - } - - // MARK: - Font.scaledCustom - - func testScaledCustomFontReturnsValue() { - let font = Font.scaledCustom("Helvetica", size: 18) - _ = font - } - - func testScaledCustomFontVariousSizes() { - for size in [8.0, 12.0, 16.0, 24.0, 48.0] as [CGFloat] { - let font = Font.scaledCustom("Arial", size: size) - _ = font - } - } - // MARK: - EnvironmentValues.screenUtil func testEnvironmentValuesDefaultIsShared() { @@ -80,18 +38,5 @@ final class SwiftUIFontEnvironmentTests: XCTestCase { env.screenUtil = custom XCTAssertTrue(env.screenUtil === custom) } - - // MARK: - Scaled sizes are consistent with shared instance - - func testScaledSystemFontUsesScaledSize() { - // Indirect verification: sp on a known value should be > 0 and finite. - let su = ScreenUtil.shared - let inputSize: CGFloat = 16 - let scaledSize = inputSize.sp - XCTAssertTrue(scaledSize.isFinite) - XCTAssertGreaterThan(scaledSize, 0) - // Font.scaledSystem internally calls .sp — so the scaled size feeds the font. - let _ = Font.scaledSystem(size: inputSize) - } } #endif