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.
- π 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
FastScalefor hot loops - π¦ Batch Scaling β
BatchScaler/withBatchScalerfor bulk work - π§ UIKit & SwiftUI β first-class support for both
- π Percentage Sizing β
.sw/.shfor 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.
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) |
|---|---|
![]() |
![]() |
Generate the Xcode project with XcodeGen:
cd Examples
xcodegen generate
open ScreenUtilExamples.xcodeproj # schemes: "SwiftUI Demo", "UIKit Demo"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")
]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)// 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)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.
.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)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, β¦).
let config = ScreenUtilConfiguration(
designSize: CGSize(width: 375, height: 812),
scalingLimits: ScalingLimits(minScale: 0.8, maxScale: 1.5)
)
// Presets: .default, .strict, .relaxed// 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)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)
}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))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))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(\.screenUtil) private var screenUtil| 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 |
// 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: BatchScalerScreenUtilDebug.printCurrentConfiguration()
#if DEBUG
ScreenUtilDebug.benchmarkScalingOperations() // DEBUG builds only
ScreenUtilDebug.showDebugOverlay(on: view) // UIKit
#endifswift build
swift test
swift test --sanitize=thread # data-race checkiPhone, iPad (incl. Split View), plus macOS / tvOS / watchOS builds. iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+.
See the Contributing Guide. Fork β branch β PR.
ScreenUtil is available under the Apache 2.0 license. See LICENSE.
- Inspired by flutter_screenutil
- Lock-free reads built on apple/swift-atomics
Made by Dicky019

