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
3 changes: 3 additions & 0 deletions Examples/Shared/Data/ProfileRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ struct LiveProfileRepository: ProfileRepository {
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()
}
}
Expand Down
25 changes: 0 additions & 25 deletions Examples/Shared/Support/Format.swift

This file was deleted.

40 changes: 40 additions & 0 deletions Examples/Shared/Utils/DeviceMetricsRows.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// DeviceMetricsRows.swift
// ScreenUtil Examples
//
// Single source of truth for the "Device & Scaling" rows, so the SwiftUI and UIKit
// cards render identical data. Snapshot fields come from the passed `metrics` (which
// SwiftUI observes, driving re-render); scale factors come from `screenUtil`.
// Created by Dicky Darmawan on 27/06/26.
//

import CoreGraphics
import ScreenUtil

/// One label/value line in the Device card.
struct MetricRow: Identifiable {
let label: String
let value: String
var id: String { label }
}

enum DeviceMetricsRows {
/// The rows describing the current device + scaling state.
static func make(metrics: ScreenMetrics, scaling screenUtil: ScreenUtil) -> [MetricRow] {
[
MetricRow(label: "device", value: "\(screenUtil.deviceType)"),
MetricRow(label: "screen", value: CGSize(width: metrics.width, height: metrics.height).dimensionsText),
MetricRow(label: "scaleW · scaleH", value: "\(screenUtil.scaleWidth.twoDecimalText) · \(screenUtil.scaleHeight.twoDecimalText)"),
MetricRow(label: "scaleText", value: screenUtil.scaleText.twoDecimalText),
MetricRow(label: "safeArea T/B", value: "\(metrics.safeAreaInsets.top.integerText) / \(metrics.safeAreaInsets.bottom.integerText)"),
MetricRow(label: "safeArea L/R", value: "\(metrics.safeAreaInsets.left.integerText) / \(metrics.safeAreaInsets.right.integerText)"),
MetricRow(label: "statusBar", value: metrics.statusBarHeight.integerText),
MetricRow(label: "screen W·H", value: "\(metrics.width.integerText) · \(metrics.height.integerText)"),
// All four ScaleType cases via the explicit entry points.
MetricRow(label: "scale(100,.width)", value: screenUtil.scale(for: 100, scaleType: .width).integerText),
MetricRow(label: "scale(100,.height)", value: screenUtil.scale(for: 100, scaleType: .height).integerText),
MetricRow(label: "scale(16,.radius)", value: screenUtil.scale(for: 16, scaleType: .radius).integerText),
MetricRow(label: "fastScale(100,.text)", value: screenUtil.fastScale(for: 100, scaleType: .text).integerText),
]
}
}
18 changes: 18 additions & 0 deletions Examples/Shared/Utils/Error+UserMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Error+UserMessage.swift
// ScreenUtil Examples
//
// Single source of truth for the user-facing load-failure message, so both demos
// show identical copy.
// Created by Dicky Darmawan on 27/06/26.
//

import Foundation

extension Error {
/// Human-readable message for the profile failure state.
var userMessage: String {
(self as? URLError)?.localizedDescription
?? "Please check your connection and try again."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ enum ExampleProfile {

/// Tag chips shown on the profile.
static let tags = ["swift", "ios", "uikit", "swiftui"]

/// Captions for the horizontal highlights carousel.
static let highlights = ["Swift", "iOS", "UIKit", "SwiftUI", "Perf", "Open"]
}
30 changes: 30 additions & 0 deletions Examples/Shared/Utils/Format.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Format.swift
// ScreenUtil Examples
//
// Value-to-display-string formatting, as extensions on the value types so they read
// with dot syntax (`width.integerText`, `repos.compactText`). Shared by both demos.
// Created by Dicky Darmawan on 21/06/26.
//

import CoreGraphics
import Foundation

extension CGFloat {
/// e.g. `123`
var integerText: String { String(format: "%.0f", self) }
/// e.g. `1.25`
var twoDecimalText: String { String(format: "%.2f", self) }
}

extension CGSize {
/// e.g. `120×80`
var dimensionsText: String { String(format: "%.0f×%.0f", width, height) }
}

extension Int {
/// Compact count, e.g. `999` / `3.4k`.
var compactText: String {
self >= 1000 ? String(format: "%.1fk", Double(self) / 1000) : "\(self)"
}
}
47 changes: 47 additions & 0 deletions Examples/Shared/Utils/Palette.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Palette.swift
// ScreenUtil Examples
//
// Centralised colour palette shared by both demo apps, exposed as extensions so it
// reads with leading-dot syntax (`.card`, `.secondaryText`) like the system colours.
// UIColor is the single source of truth; the SwiftUI `Color` values derive from it.
// Created by Dicky Darmawan on 27/06/26.
//

import SwiftUI
import UIKit

// MARK: - UIColor source of truth (used by the UIKit demo)

extension UIColor {
/// Banner brand gradient (top-leading → bottom-trailing).
static let bannerColors: [UIColor] = [.systemBlue, .systemPurple]
/// Highlight-circle gradient (top-light → accent).
static let accentColors: [UIColor] = [UIColor.systemBlue.withAlphaComponent(0.85), .systemBlue]

static let card = UIColor.secondarySystemBackground
static let chip = UIColor.systemBlue.withAlphaComponent(0.15)
static let avatarFill = UIColor.secondarySystemBackground
static let avatarStroke = UIColor.systemBackground
static let secondaryText = UIColor.secondaryLabel
static let onAccent = UIColor.white
}

// MARK: - SwiftUI mirrors (derived — one definition per colour)

/// Single colours on `ShapeStyle where Self == Color`, so they work with leading-dot
/// in style contexts: `.background(.card)`, `.foregroundStyle(.secondaryText)`.
extension ShapeStyle where Self == Color {
static var card: Color { Color(uiColor: .card) }
static var chip: Color { Color(uiColor: .chip) }
static var avatarFill: Color { Color(uiColor: .avatarFill) }
static var avatarStroke: Color { Color(uiColor: .avatarStroke) }
static var secondaryText: Color { Color(uiColor: .secondaryText) }
static var onAccent: Color { Color(uiColor: .onAccent) }
}

/// Gradient colour arrays (no leading-dot for `[Color]`, so referenced as `Color.bannerColors`).
extension Color {
static var bannerColors: [Color] { UIColor.bannerColors.map(Color.init(uiColor:)) }
static var accentColors: [Color] { UIColor.accentColors.map(Color.init(uiColor:)) }
}
43 changes: 21 additions & 22 deletions Examples/SwiftUIDemo/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,30 @@ struct ProfileView: View {
}

var body: some View {
Group {
switch model.state {
case .idle, .loading:
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityLabel("Loading profile")
case .failed(let error):
ContentUnavailableView {
Label("Couldn't Load Profile", systemImage: "wifi.slash")
} description: {
Text(Self.message(for: error))
} actions: {
Button("Retry") { Task { await model.retry() } }
NavigationStack {
Group {
switch model.state {
case .idle, .loading:
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityLabel("Loading profile")
case .failed(let error):
ContentUnavailableView {
Label("Couldn't Load Profile", systemImage: "wifi.slash")
} description: {
Text(error.userMessage)
} actions: {
Button("Retry") { Task { await model.retry() } }
}
case .loaded(let profile):
loadedContent(profile)
}
case .loaded(let profile):
loadedContent(profile)
}
.navigationTitle("SwiftUI Demo")
.navigationBarTitleDisplayMode(.inline)
.task { await model.load() }
}
.task { await model.load() }
}

private func loadedContent(_ profile: Profile) -> some View {
Expand All @@ -56,11 +60,6 @@ struct ProfileView: View {
.padding(.top, 12.h)
}
}

private static func message(for error: Error) -> String {
(error as? URLError)?.localizedDescription
?? "Please check your connection and try again."
}
}

#Preview("Loaded") {
Expand Down
55 changes: 5 additions & 50 deletions Examples/SwiftUIDemo/Sections/DeviceCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,66 +22,21 @@ struct DeviceCard: View {
Text("Device & Scaling")
.font(.system(size: 17.sp, weight: .semibold))

row("device", "\(screenUtil.deviceType)")
row("screen", Format.describe(CGSize(width: metrics.width, height: metrics.height)))
row("scaleW · scaleH", "\(Format.twoDecimals(screenUtil.scaleWidth)) · \(Format.twoDecimals(screenUtil.scaleHeight))")
row("scaleText", Format.twoDecimals(screenUtil.scaleText))
row("safeArea T/B", "\(Format.integer(metrics.safeAreaInsets.top)) / \(Format.integer(metrics.safeAreaInsets.bottom))")
row("safeArea L/R", "\(Format.integer(metrics.safeAreaInsets.left)) / \(Format.integer(metrics.safeAreaInsets.right))")
row("statusBar", Format.integer(metrics.statusBarHeight))
row("screen W·H", "\(Format.integer(metrics.width)) · \(Format.integer(metrics.height))")
// Explicit scaling entry points (numeric sugar wraps these) — all four ScaleType cases.
row("scale(100,.width)", Format.integer(screenUtil.scale(for: 100, scaleType: .width)))
row("scale(100,.height)", Format.integer(screenUtil.scale(for: 100, scaleType: .height)))
row("scale(16,.radius)", Format.integer(screenUtil.scale(for: 16, scaleType: .radius)))
row("fastScale(100,.text)", Format.integer(screenUtil.fastScale(for: 100, scaleType: .text)))
// Honest self-test exercising the bulk + per-call geometry APIs that have no natural
// home in declarative layout (kept to one labelled line, not faked into the UI).
row("bulk self-test", selfTest())
ForEach(DeviceMetricsRows.make(metrics: metrics, scaling: screenUtil)) { item in
row(item.label, item.value)
}
}
.padding(16.w)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.quaternary, in: .rect(cornerRadius: 16.r))
.background(.card, in: .rect(cornerRadius: 16.r))
}

private func row(_ key: String, _ value: String) -> some View {
HStack {
Text(key).foregroundStyle(.secondary)
Text(key).foregroundStyle(.secondaryText)
Spacer()
Text(value).fontWeight(.medium)
}
.font(.system(size: 13.sp))
}

/// Exercises every bulk/geometry API that has no natural home in declarative SwiftUI layout,
/// so the showcase still demonstrates 100% of the public surface. One line, honestly labelled.
private func selfTest() -> String {
let batch = ScreenUtil.shared.batchScaler
let widths = batch.widths([10, 20, 30])
let heights = batch.heights([10, 20])
let fonts = batch.fontSizes([12, 14])
let radii = batch.radii([8, 12])
let sizes = batch.sizes([CGSize(width: 10, height: 10)])
let points = batch.points([CGPoint(x: 10, y: 10), CGPoint(x: 20, y: 20)])
let rects = batch.rects([CGRect(x: 0, y: 0, width: 50, height: 50)])
let byHeight = batch.scale([100, 200], scaleType: .height)

let fast = ScreenUtil.shared.fastScale
_ = fast.size(CGSize(width: 10, height: 10))
_ = fast.point(CGPoint(x: 5, y: 5))
_ = fast.rect(CGRect(x: 0, y: 0, width: 8, height: 8))
_ = withFastScale { $0.width(20) }

// CGGeometry helpers (static + instance forms).
_ = CGSize.scaled(width: 8, height: 8)
_ = CGSize(width: 8, height: 8).fastScaled()
_ = CGRect.scaled(x: 0, y: 0, width: 10, height: 10)
_ = CGRect.responsive(x: 0, y: 0, width: 10, height: 10)
_ = CGPoint.responsive(x: 4, y: 4)
_ = CGPoint(x: 4, y: 4).fastScaled()

let count = widths.count + heights.count + fonts.count + radii.count
+ sizes.count + points.count + rects.count + byHeight.count
return "\(count) bulk values ✓"
}
}
7 changes: 4 additions & 3 deletions Examples/SwiftUIDemo/Sections/Highlights.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@ import ScreenUtil
import SwiftUI

struct Highlights: View {
private let titles = ["Swift", "iOS", "UIKit", "SwiftUI", "Perf", "Open"]
private let titles = ExampleProfile.highlights

var body: some View {
// Capture scale factors once for the repeated circles (hot path).
let fast = ScreenUtil.shared.fastScale
let diameter = fast.width(64)

ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: fast.width(14)) {
ForEach(titles, id: \.self) { title in
VStack(spacing: fast.height(6)) {
Circle()
.fill(Color.accentColor.gradient)
.fill(LinearGradient(colors: Color.accentColors, startPoint: .top, endPoint: .bottom))
.frame(width: diameter, height: diameter)
.overlay(Text(title.prefix(1)).font(.system(size: fast.text(24), weight: .bold)).foregroundStyle(.white))
.overlay(Text(title.prefix(1)).font(.system(size: fast.text(24), weight: .bold)).foregroundStyle(.onAccent))
Text(title).font(.system(size: fast.text(12)))
}
}
Expand Down
8 changes: 4 additions & 4 deletions Examples/SwiftUIDemo/Sections/ProfileHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ struct ProfileHeader: View {
var body: some View {
VStack(alignment: .leading, spacing: 12.h) {
ZStack(alignment: .bottomLeading) {
LinearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing)
LinearGradient(colors: Color.bannerColors, startPoint: .topLeading, endPoint: .bottomTrailing)
.frame(height: 140.h)
.clipShape(.rect(cornerRadius: 20.r))

AsyncImage(url: profile.avatar) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray.opacity(0.3)
Color.avatarFill
}
.frame(width: avatarSize.width, height: avatarSize.height)
.clipShape(.circle)
.overlay(Circle().strokeBorder(.background, lineWidth: 4.w))
.overlay(Circle().strokeBorder(.avatarStroke, lineWidth: 4.w))
.offset(x: 20.w, y: avatarSize.height / 2)
}
.padding(.bottom, avatarSize.height / 2)
Expand All @@ -39,7 +39,7 @@ struct ProfileHeader: View {
.font(.system(size: 24.sp, weight: .bold))
Text("@\(profile.username)")
.font(.system(size: 15.sp))
.foregroundStyle(.secondary)
.foregroundStyle(.secondaryText)
if let bio = profile.bio {
Text(bio)
.font(.system(size: 15.sp))
Expand Down
Loading
Loading