Last active
May 12, 2021 12:09
-
-
Save marcpalmer/73ea6178228cbda090a088561c2b05ed to your computer and use it in GitHub Desktop.
Example of adaptive values in SwiftUI where size classes determine the value you want to use
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import SwiftUI | |
/// A value that can have different values depending on compact or regular size class in a single axis. | |
/// | |
/// Usage example: | |
/// | |
/// ``` | |
/// static let textPadding = SizeClassDependentValue(compact: 20, regular: 50) | |
/// | |
/// var body: some View { | |
/// HStack { | |
/// Text("Hello").adaptivePadding(Self.textPadding) | |
/// } | |
/// } | |
/// ``` | |
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 | |
} | |
/// 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) | |
} | |
} | |
/// Add negation support for floating point values, so that you can use e.g. `-paddingAmount` | |
extension SizeClassDependentValue where Value: FloatingPoint { | |
public 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. | |
extension View { | |
/// Like `padding` but using a value that is dependent on size class | |
@inlinable 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