Skip to content

Instantly share code, notes, and snippets.

@marcpalmer
Created December 24, 2024 18:48
Show Gist options
  • Save marcpalmer/b593fb2b6e9502a77d19dfc249d360e0 to your computer and use it in GitHub Desktop.
Save marcpalmer/b593fb2b6e9502a77d19dfc249d360e0 to your computer and use it in GitHub Desktop.
Size-dependent metrics for SwiftUI
//
// SizeClassDependentValue.swift
// Captionista
//
// Created by Marc Palmer on 11/05/2021.
// Copyright © 2021 Montana Floss Co. Ltd. All rights reserved.
//
import Foundation
import SwiftUI
/// A value that can have different values depending on compact or regular size class in a single axis.
public struct SizeClassDependentValue<Value> {
private let compact: Value
private let regular: Value
let axis: Axis
/// Create using the values for the given axis.
public init(_ axis: Axis = .horizontal, compact: Value, regular: Value? = nil) {
self.axis = axis
self.compact = compact
self.regular = regular ?? compact
}
public init(_ value: Value) {
self.init(compact: value, regular: value)
}
/// Get the value for the given size class.
/// - note: If the sizeClass is nil it will default to the value for regular
public func value(for sizeClass: UserInterfaceSizeClass?) -> Value {
switch sizeClass {
case .compact:
return compact
case .none,
.regular:
return regular
@unknown default:
return regular
}
}
/// Syntactic sugar for getting the value for the size class.
public subscript(_ sizeClass: UserInterfaceSizeClass?) -> Value {
value(for: sizeClass)
}
}
extension SizeClassDependentValue: ExpressibleByFloatLiteral where Value: BinaryFloatingPoint {
public init(floatLiteral value: FloatLiteralType) {
self.init(Value(value))
}
}
extension SizeClassDependentValue: ExpressibleByIntegerLiteral where Value: BinaryFloatingPoint {
public init(integerLiteral value: IntegerLiteralType) {
self.init(Value(value))
}
}
/// Add negation support for floating point values, so that you can use e.g. `-paddingAmount`
public extension SizeClassDependentValue where Value: FloatingPoint {
static var zero: Self { SizeClassDependentValue<Value>(compact: 0) }
static prefix func -(value: SizeClassDependentValue<Value>) -> SizeClassDependentValue<Value> {
SizeClassDependentValue(value.axis, compact: -(value.compact), regular: -(value.regular))
}
}
/// Add an `adaptivePadding` View modifier that takes size class dependent values and evaluates them automatically
/// so that your View does not have to include the size class environment values.
public extension View {
/// Like `padding` but using a value that is dependent on size class
func adaptivePadding(_ edges: Edge.Set = .all,
_ length: SizeClassDependentValue<CGFloat>) -> some View {
modifier(SizeClassDependentPadding(edges: edges, length: length))
}
}
/// The adaptive padding modifier implementation
public struct SizeClassDependentPadding: ViewModifier {
let edges: Edge.Set
let length: SizeClassDependentValue<CGFloat>
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
public init(edges: Edge.Set, length: SizeClassDependentValue<CGFloat>) {
self.edges = edges
self.length = length
}
public func body(content: Content) -> some View {
/// Match the size class used to the intended axis of the value.
/// This allows the value to determine what axis affects it, and means you can use
/// values that are affected by `horizontal` size classes for both horizontal or vertical padding, e.g.
/// your padding at the sides will often match padding at the top, but they are both based on the
/// screen width.
let length = self.length.value(for: length.axis == .horizontal ? horizontalSizeClass : verticalSizeClass)
return content.padding(edges, length)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment