Skip to content

Repository files navigation

ScreenUtil

Swift Version Platforms SPM Compatible License

A thread-safe Swift package for responsive screen adaptation on Apple platforms.
Scale your UI from a fixed design size (e.g. 375Γ—812 from Figma) to the real device. Inspired by flutter_screenutil.

✨ Features

  • πŸ”’ 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
  • πŸ“¦ 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

Dependencies: one β€” 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/ β€” 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:

cd Examples
xcodegen generate
open ScreenUtilExamples.xcodeproj   # schemes: "SwiftUI Demo", "UIKit Demo"

πŸ“¦ Installation

Swift Package Manager

In Xcode: File β†’ Add Package Dependencies… and enter:

https://github.com/Dicky019/ScreenUtil

Or in Package.swift:

dependencies: [
    .package(url: "https://github.com/Dicky019/ScreenUtil", from: "1.0.0")
]

πŸš€ Quick Start

1. Configure once at launch

import ScreenUtil

// e.g. in AppDelegate / App init
ScreenUtil.shared.configure(with: ScreenUtilConfiguration(
    designSize: CGSize(width: 375, height: 812), // your design canvas
    minTextAdapt: true
))

// Or use a device preset:
ScreenUtil.shared.configure(with: .iPhone13Pro)

2. Scale values

// UIKit
view.frame = CGRect(x: 20.w, y: 50.h, width: 200.w, height: 100.h)
label.font = .systemFont(ofSize: 16.sp)
button.layer.cornerRadius = 12.r

// SwiftUI
Text("Hello World")
    .font(.system(size: 16.sp))
    .frame(width: 200.w, height: 50.h)
    .padding(.horizontal, 20.w).padding(.vertical, 20.h)

πŸ“– Usage

Basic scaling

100.w   // width  = 100 * (deviceWidth  / designWidth)
50.h    // height = 50  * (deviceHeight / designHeight)
16.sp   // font   = min(widthScale, heightScale) ratio (prevents distortion)
12.r    // radius = min(widthScale, heightScale) ratio

50.sw   // 50% of screen width
10.sh   // 10% of screen height

.w, .h, .sp, .r, .sw, .sh are available on Int, Float, Double, and CGFloat.

Fast path for hot loops

.w validates its input (NaN/inf β†’ 0). For tight per-frame loops, capture the factors once with FastScale:

withFastScale { fast in
    for particle in particles {
        particle.x = fast.width(particle.x)
        particle.y = fast.height(particle.y)
    }
}

// Or grab it directly:
let fast = ScreenUtil.shared.fastScale
view.frame = fast.rect(designRect)

Batch scaling

let widths: [CGFloat] = [100, 200, 300, 400]
let scaled = ScreenUtil.shared.batchScaler.widths(widths)   // one factor lookup for all

// Reusable scaler:
let scaler = ScreenUtil.shared.batchScaler
let heights = scaler.heights([10, 20, 30])
let fonts   = scaler.fontSizes([12, 14, 16])

BatchScaler accepts any [T: Numeric] (Int, Double, CGFloat, Int64, …).

Scaling limits

let config = ScreenUtilConfiguration(
    designSize: CGSize(width: 375, height: 812),
    scalingLimits: ScalingLimits(minScale: 0.8, maxScale: 1.5)
)
// Presets: .default, .strict, .relaxed

πŸ“± UIKit

// Fonts
label.font = .systemFont(ofSize: 16, weight: .medium, scaled: true)
let custom = UIFont.customFont(name: "Helvetica", size: 14)        // UIFont? (scaled)
let scaled = existingFont.scaled()

// Auto Layout helpers (return the created constraint(s))
view.width(200)
view.height(100)
view.size(width: 200, height: 100)
view.cornerRadius(12)
view.borderWidth(1)

// Update a constraint with scaling (axis inferred from its attribute)
widthConstraint.updateConstant(120)

// Insets
let insets = UIEdgeInsets.scaled(all: 16)
let sym    = UIEdgeInsets.scaled(horizontal: 20, vertical: 10)

Collection view example

func collectionView(_ collectionView: UICollectionView,
                    layout: UICollectionViewLayout,
                    sizeForItemAt indexPath: IndexPath) -> CGSize {
    let spacing = 16.w
    let columns: CGFloat = ScreenUtil.shared.deviceType == .iPad ? 4 : 2
    let totalSpacing = spacing * (columns + 1)
    let itemWidth = (ScreenUtil.shared.screenWidth - totalSpacing) / columns
    return CGSize(width: itemWidth, height: itemWidth * 1.3)
}

🎨 SwiftUI

Scaling inside native modifiers

Apply the .w/.h/.sp/.r properties directly inside SwiftUI's own modifiers β€” no wrappers needed:

Image(systemName: "star.fill")
    .frame(width: 60.w, height: 60.h)

VStack {
    Text("Welcome").font(.system(size: 28.sp, weight: .bold))
}
.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:

Text("Title").font(.system(size: 24.sp, weight: .bold))
Text("Body").font(.custom("Helvetica", size: 16.sp))

Scaling values

Keep design values as plain constants and scale them at the call site with .w/.h/.sp/.sw:

struct CardView: View {
    private let cardWidth: CGFloat = 300
    private let cardHeight: CGFloat = 200
    private let titleSize: CGFloat = 24

    var body: some View {
        VStack { Text("Card").font(.system(size: titleSize.sp)) }
            .frame(width: cardWidth.w, height: cardHeight.h)
    }
}

Environment

@Environment(\.screenUtil) private var screenUtil

πŸ“Š API Reference

Numeric extensions

Extension Description Example
.w Width scaling (validated) 100.w
.h Height scaling (validated) 50.h
.sp Font scaling (no distortion) 16.sp
.r Radius scaling 12.r
.sw Screen width percentage 50.sw
.sh Screen height percentage 10.sh

ScreenUtil.shared

// Configuration
func configure(with: ScreenUtilConfiguration)
func refreshMetrics()
func getScreenMetrics() -> ScreenMetrics

// Scaling
func scale(for: CGFloat, scaleType: ScaleType) -> CGFloat
func fastScale(for: CGFloat, scaleType: ScaleType) -> CGFloat

// Dimensions & factors (lock-free reads)
var screenWidth / screenHeight: CGFloat
var safeAreaTop / safeAreaBottom / safeAreaLeft / safeAreaRight: CGFloat
var statusBarHeight: CGFloat
var scaleWidth / scaleHeight / scaleText: CGFloat
var deviceType: DeviceType          // .iPhone / .iPad / .mac / .tv / .watch

// Performance
var fastScale: FastScale
var batchScaler: BatchScaler

πŸ› Debugging

ScreenUtilDebug.printCurrentConfiguration()

#if DEBUG
ScreenUtilDebug.benchmarkScalingOperations()  // DEBUG builds only
ScreenUtilDebug.showDebugOverlay(on: view)    // UIKit
#endif

πŸ§ͺ Testing

swift build
swift test
swift test --sanitize=thread   # data-race check

πŸ“± Device Support

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

🀝 Contributing

See the Contributing Guide. Fork β†’ branch β†’ PR.

πŸ“„ License

ScreenUtil is available under the Apache 2.0 license. See LICENSE.

πŸ™ Acknowledgments


Made by Dicky019

About

Thread-safe responsive screen adaptation for Apple platforms (iOS, macOS, tvOS, watchOS). Scale UI from a fixed design size to the real device. Inspired by flutter_screenutil.

Topics

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages