A lot of StackOverflow answers for dealing with things in SwiftUI sometimes being positioned not how you want (for example) understandably solve it with a magic number to put something where they want.
The downside of this is that it likely only works at the default Dynamic Type size, so anyone with larger or smaller text settings (for accessibility reasons, for instance) will likely have a layout that doesn't look as optimized (for instance, if you assume 24.0
is a good number based on the default .large
size, if they have their phone set to accessibilityExtraExtraExtraLarge
, 24.0
will likely be way too small).
So here's a quick helper extension that simply scales a number based on the Dynamic Type size, so that 24.0
will increase and decrease with the user's Dynamic Type setting.
// Extending BinaryFloatingPoint so that this extension works with both `CGFloat` and `Double`.
extension BinaryFloatingPoint {
func scaled(to dynamicTypeSize: DynamicTypeSize) -> CGFloat {
// Important that you pass in the trait collection to `UIFontMetrics`, because otherwise it'll always use the system setting, which you may have overriden for a `UIHostingController` (for instance if you have in-app font options independent of the system).
let traitCollection = UITraitCollection(preferredContentSizeCategory: dynamicTypeSize.contentSizeCategory)
// UIFontMetrics does most of the heavy lifting by scaling the number up or down
let scaled = UIFontMetrics.default.scaledValue(for: CGFloat(self), compatibleWith: traitCollection)
return scaled
}
}
// UIFont's scaling doesn't have a function that accepts SwiftUI's `DynamicTypeSize`, so convert it to UIKit's `UIContentSizeCategory`.
extension DynamicTypeSize {
var contentSizeCategory: UIContentSizeCategory {
switch self {
case .xSmall:
return .extraSmall
case .small:
return .small
case .medium:
return .medium
case .large:
return .large
case .xLarge:
return .extraLarge
case .xxLarge:
return .extraExtraLarge
case .xxxLarge:
return .extraExtraExtraLarge
case .accessibility1:
return .accessibilityMedium
case .accessibility2:
return .accessibilityLarge
case .accessibility3:
return .accessibilityExtraLarge
case .accessibility4:
return .accessibilityExtraExtraLarge
case .accessibility5:
return .accessibilityExtraExtraExtraLarge
@unknown default:
assertionFailure("Unknown content size category encountered: \(self)")
return .large
}
}
}
Here's a usage example:
struct ContentView: View {
@Environment(\.dynamicTypeSize) private var dynamicTypeSize: DynamicTypeSize
let offsetX: CGFloat {
// This will return, for instance, -33.0 when Dynamic Type is set to XXXLarge
-25.0.scaled(to: dynamicTypeSize))
}
var body: some View {
VStack {
Text("\(offsetX)")
List {
ForEach(0 ..< 10, id: \.self) { index in
Text("Move me!")
.listRowInsets(.init(top: 0.0, leading: offsetX, bottom: 0.0, trailing: 0.0))
}
.onMove(perform: { _, _ in })
}
.environment(\.editMode, .constant(.active))
}
}
}
(This will immediately respond to any system Dynamic Type changes or any UITraitCollection
overrides)
Hahaha whoops, SwiftUI has this built in! Just use
@ScaledMetric
! https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-scaledmetric-property-wrapper