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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ignore build artifacts and Xcode project files
build/*

# XcodeGen-generated demo project (regenerate with `xcodegen generate`)
ScreenUtilExamples.xcodeproj/*
8 changes: 8 additions & 0 deletions Examples/.swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"version": 1,
"indentation": { "spaces": 4 },
"lineLength": 160,
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeEachArgument": false
}
159 changes: 34 additions & 125 deletions Examples/README.md
Original file line number Diff line number Diff line change
@@ -1,140 +1,49 @@
# ScreenUtil Examples
# ScreenUtil Demo Apps

This directory contains comprehensive examples demonstrating how to use ScreenUtil in both UIKit and SwiftUI applications.
Two runnable iOS 17 demo apps — one pure SwiftUI, one pure UIKit — each a single
polished **GitHub profile page** whose every dimension flows through ScreenUtil.
Designed on the **iPhone 12 baseline (390×844)**: on an iPhone 12 the UI renders
1:1, and on a larger device (e.g. iPhone 17 Pro Max) the same design scales up
proportionally. Screenshot both to show the adaptation.

## Files
## Run

### UIKitExample.swift
Complete UIKit implementation examples including:

#### Basic Usage
- **BasicUIKitViewController**: Demonstrates fundamental ScreenUtil usage
- Responsive layout with `.w`, `.h`, `.sp`, `.r` extensions
- Auto Layout with scaled constraints
- Screen metrics access and display
- Configuration setup

#### Advanced Features
- **AdvancedUIKitViewController**: Shows advanced capabilities
- Batch operations for scaling multiple values
- Fast scaling with `FastScale` struct
- Custom scaling limits demonstration
- Performance benchmarking
- ScrollView with multiple sections

#### Custom Components
- **ResponsiveCardView**: Custom UIView with built-in responsive design
- Responsive images, labels, and buttons
- Auto Layout with ScreenUtil scaling
- Shadow and corner radius scaling

### SwiftUIExample.swift
Complete SwiftUI implementation examples including:

#### Basic SwiftUI Views
- **BasicSwiftUIView**: Core SwiftUI usage patterns
- `.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
- **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
- **ResponsiveProfileCard**: Professional profile card
- **ResponsiveProgressBar**: Animated progress indicator
- **ResponsiveStatsView**: Statistics grid layout
- **PerformanceDemoView**: Real-time performance testing

## Key Features Demonstrated

### 1. Basic Scaling
```swift
// UIKit
button.widthAnchor.constraint(equalToConstant: 100.w)
label.font = .systemFont(ofSize: 16.sp)
view.layer.cornerRadius = 8.r

// SwiftUI
Text("Hello").font(.system(size: 16.sp))
Rectangle().frame(width: 100.w, height: 50.h)
```bash
cd Examples
brew install xcodegen # one-time
xcodegen generate
open ScreenUtilExamples.xcodeproj
```

### 2. Configuration
```swift
// Set design size (e.g., iPhone 13 Pro)
ScreenUtil.shared.configure(with: .iPhone13Pro)
Pick a scheme and run:

// Custom configuration
let config = ScreenUtilConfiguration(
designSize: CGSize(width: 390, height: 844),
minTextAdapt: true,
scalingLimits: .default
)
ScreenUtil.shared.configure(with: config)
```
- **ScreenUtilSwiftUIDemo** — pure SwiftUI (`@Observable` model, `NavigationStack`, `@Environment(\.screenUtil)`).
- **ScreenUtilUIKitDemo** — pure UIKit (`UIScene`, Auto Layout, stored-property subviews).

### 3. Batch Operations
```swift
let values = [10, 20, 30, 40, 50]
let scaledWidths = ScreenUtil.shared.batchScaler.widths(values)
let scaledHeights = ScreenUtil.shared.batchScaler.heights(values)
```

### 4. Fast Scaling
```swift
let fastScale = ScreenUtil.shared.fastScale
let scaledSize = fastScale.size(CGSize(width: 100, height: 50))
let scaledPoint = fastScale.point(CGPoint(x: 10, y: 20))
```
## What it shows

### 5. Screen Metrics
```swift
let metrics = ScreenUtil.shared.getScreenMetrics()
print("Screen: \(metrics.width) x \(metrics.height)")
print("Safe Area Top: \(metrics.safeAreaInsets.top)")
print("Device: \(ScreenUtil.shared.deviceType)")
```

### 6. Debug Tools
```swift
// Print current configuration
ScreenUtilDebug.printCurrentConfiguration()

// Run performance benchmark
ScreenUtilDebug.benchmarkScalingOperations()
```
Each app composes the same sections, all scaled from the design baseline:

## Usage Instructions
| Section | ScreenUtil APIs exercised |
|---------|---------------------------|
| **Header** (banner, avatar, name, bio) | numeric `.w .h .sp .r`, `CGSize.scaled`, `UIView.cornerRadius/borderWidth/size`, `UIFont.systemFont(…, scaled:)` |
| **Stats row** (repos/followers/following) | `BatchScaler.fontSizes`, `UIEdgeInsets.scaled` |
| **Highlights** (horizontal strip) | `FastScale` (`width/height/text/size/point/rect`), `withFastScale`, `BatchScaler.points/.rects` |
| **Tag chips** | `BatchScaler.widths/.radii`, `withBatchScaler` |
| **Device & Scaling card** | `getScreenMetrics`, `screenWidth/Height`, `scaleWidth/Height/Text`, `deviceType`, `safeArea*`, `statusBarHeight`, `scale/fastScale(for:scaleType:)` (all `ScaleType` cases), `UIFont.customFont`, and a one-line "bulk self-test" covering `BatchScaler.heights/.sizes/.scale`, `BatchScaler.edgeInsets` (UIKit), `CGRect.scaled/.responsive` |

### For UIKit Projects
1. Import ScreenUtil in your view controllers
2. Configure ScreenUtil in your app launch or viewDidLoad
3. Use the scaling extensions (.w, .h, .sp, .r) throughout your UI code
4. Copy and adapt the example view controllers as needed
### Real data + offline-safe

### For SwiftUI Projects
1. Import ScreenUtil in your SwiftUI views
2. Configure ScreenUtil in your app startup or view onAppear
3. Use the font and view modifiers for responsive design
4. Leverage property wrappers for automatic scaling
5. Copy and adapt the example views as needed
The profile loads from the live GitHub public API
(`api.github.com/users/{login}`) — real avatar, repo and follower counts. On any
failure (offline / rate-limited) it falls back to a bundled `sample-user.json`,
so the demo (and your screenshots) never show a spinner or empty state.

## Performance Notes
## Verify the scaling

- Fast scaling operations are ~1.4x faster than standard scaling
- Batch operations are optimized for processing multiple values
- All operations are thread-safe and can be used from any queue
- Memory usage is minimal with efficient caching strategies
Open the **Device & Scaling** card and read `scaleW · scaleH`:

## Best Practices
- iPhone 12 → `1.00 · 1.00` (renders exactly as designed)
- iPhone 17 Pro Max → ~`1.13 · 1.13` (everything scaled up proportionally)

1. **Configure Early**: Set up ScreenUtil configuration as early as possible in your app lifecycle
2. **Use Appropriate Scale Types**: Choose .w for widths, .h for heights, .sp for text, .r for radii
3. **Leverage Batch Operations**: Use batch scaling when processing multiple values
4. **Test on Multiple Devices**: Verify your responsive design on different screen sizes
5. **Monitor Performance**: Use the built-in debugging tools to optimize performance
> The generated `ScreenUtilExamples.xcodeproj` is not committed; regenerate it with `xcodegen generate`.
45 changes: 45 additions & 0 deletions Examples/Shared/Data/GitHubUser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// GitHubUser.swift
// ScreenUtil Examples
//
// GitHub public-user DTO (mirrors the API JSON) + mapping to the domain Profile.
// Created by Dicky Darmawan on 27/06/26.
//

import Foundation

/// A GitHub public user, decoded from `api.github.com/users/{login}`.
struct GitHubUser: Codable, Equatable, Sendable {
let login: String
let name: String?
let bio: String?
let avatarURL: URL
let publicRepos: Int
let followers: Int
let following: Int
let location: String?
let company: String?
let blog: String?

enum CodingKeys: String, CodingKey {
case login, name, bio, followers, following, location, company, blog
case avatarURL = "avatar_url"
case publicRepos = "public_repos"
}
}

extension GitHubUser {
/// Maps the API DTO to the domain `Profile`. Resolves the display name once
/// (`name` falls back to `login`) so views never repeat the `?? login` dance.
func toDomain() -> Profile {
Profile(
username: login,
name: name ?? login,
bio: bio,
avatar: avatarURL,
repos: publicRepos,
followers: followers,
following: following
)
}
}
44 changes: 44 additions & 0 deletions Examples/Shared/Data/ProfileRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ProfileRepository.swift
// ScreenUtil Examples
//
// Data layer: abstracts profile loading behind a protocol (Dependency Inversion).
// Created by Dicky Darmawan on 27/06/26.
//

import Foundation

/// Loads a `Profile`. Abstraction the view model depends on (not URLSession).
protocol ProfileRepository: Sendable {
func fetch() async throws -> Profile
}

/// Live implementation: GitHub public API → `GitHubUser` DTO → `Profile`.
/// Throws on bad URL, non-200, transport error, or decode failure (no fallback).
struct LiveProfileRepository: ProfileRepository {
let username: String
var session: URLSession = .shared

func fetch() async throws -> Profile {
guard let url = URL(string: "https://api.github.com/users/\(username)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(GitHubUser.self, from: data).toDomain()
}
}

/// Stub for SwiftUI previews and unit tests (no network). Holds a `@Sendable`
/// closure rather than a `Result<Profile, any Error>` — `any Error` is not
/// Sendable, so a stored Result would break `ProfileRepository: Sendable`; a
/// `@Sendable` function type is Sendable and still lets callers throw any error.
struct StubProfileRepository: ProfileRepository {
private let handler: @Sendable () throws -> Profile
init(_ handler: @escaping @Sendable () throws -> Profile) { self.handler = handler }
func fetch() async throws -> Profile { try handler() }
}
24 changes: 24 additions & 0 deletions Examples/Shared/Domain/LoadState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// LoadState.swift
// ScreenUtil Examples
//
// Generic async-load state machine driving the profile screens.
// Created by Dicky Darmawan on 27/06/26.
//

/// The state of an asynchronously loaded value.
///
/// Not `Sendable`: `failed(Error)` wraps `any Error` (not Sendable). This type is
/// mutated and read only on `@MainActor`, so it never crosses an actor boundary.
enum LoadState<Value> {
case idle
case loading
case loaded(Value)
case failed(Error)

/// The loaded value, or `nil` in any other state.
var value: Value? {
if case .loaded(let value) = self { return value }
return nil
}
}
33 changes: 33 additions & 0 deletions Examples/Shared/Domain/Profile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Profile.swift
// ScreenUtil Examples
//
// Domain model the views render — mapped from the GitHubUser DTO.
// Created by Dicky Darmawan on 27/06/26.
//

import Foundation

/// A GitHub profile, in domain terms the UI cares about (no serialization detail).
struct Profile: Equatable, Sendable {
let username: String
let name: String
let bio: String?
let avatar: URL
let repos: Int
let followers: Int
let following: Int
}

extension Profile {
/// Self-contained sample for previews and tests (no network, no bundle).
static let preview = Profile(
username: "octocat",
name: "The Octocat",
bio: "Responsive layouts, scaled with ScreenUtil.",
avatar: URL(string: "https://avatars.githubusercontent.com/u/583231")!,
repos: 8,
followers: 3400,
following: 180
)
}
19 changes: 19 additions & 0 deletions Examples/Shared/Support/ExampleProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// ExampleProfile.swift
// ScreenUtil Examples
//
// Single source of truth for what both demos render + the ScreenUtil baseline,
// so the UIKit and SwiftUI showcases stay in lockstep.
// Created by Dicky Darmawan on 27/06/26.
//

/// Shared facts driving both profile apps. Centralised here so the two showcases
/// never drift apart. (The ScreenUtil baseline stays inline in each app entry point
/// on purpose, so the `configure(with:)` call is visible as usage documentation.)
enum ExampleProfile {
/// GitHub handle both apps load.
static let username = "Dicky019"

/// Tag chips shown on the profile.
static let tags = ["swift", "ios", "uikit", "swiftui"]
}
25 changes: 25 additions & 0 deletions Examples/Shared/Support/Format.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Format.swift
// ScreenUtil Examples
//
// Single source of truth for the demo's value formatting (shared by both apps).
// Created by Dicky Darmawan on 21/06/26.
//

import CoreGraphics
import Foundation

/// Formatting helpers used across the demo to render scaled values.
enum Format {
/// e.g. `123`
static func integer(_ value: CGFloat) -> String { String(format: "%.0f", value) }

/// e.g. `123.4`
static func oneDecimal(_ value: CGFloat) -> String { String(format: "%.1f", value) }

/// e.g. `1.25`
static func twoDecimals(_ value: CGFloat) -> String { String(format: "%.2f", value) }

/// e.g. `120×80`
static func describe(_ size: CGSize) -> String { String(format: "%.0f×%.0f", size.width, size.height) }
}
Loading
Loading