As part of my efforts to improve accessibility in ScreenCred, I've been working at improving layouts for large Dynamic Type sizes. To help with this, I'm trying to use ViewThatFits
. It works as I'd expect in most places. However, I had a lot of issues when trying to use it within a ScrollView for repeating items. I'm sure that a lot of my issues are from not really knowing how ViewThatFits
chooses which view to use. What is "fits"? I'm not 100% sure.
In ScreenCreds, I have a couple places with lists and I want every item in that list to either be a horizontal layout or a vertical layout, but not a mix of both.
On an individual view, ViewThatFits works real well:
ViewThatFits {
HStack {
Image(item.image)
Text(item.label)
}
VStack {
Image(item.image)
Text(item.label)
}
}
If the text gets too long, it will switch to use the VStack View. But, if I put that in a ForEach, which view that fits is dependent on the content, so some items may use HStack and others will VStack. Not what I want.
ScrollView {
ForEach(item) { item in
ViewThatFits {
HStack {
Image(item.image)
Text(item.label)
}
VStack {
Image(item.image)
Text(item.label)
}
}
}
}
So I thought, easy, just put two ForEaches:
ScrollView {
ViewThatFits {
ForEach(item) { item in
HStack {
Image(item.image)
Text(item.label)
}
}
ForEach(item) { item in
VStack {
Image(item.image)
Text(item.label)
}
}
}
}
Unfortunately, this did not work. No matter what I tried, it always picked the second View. The only way I found to fix this is to switch between ScrollViews:
ViewThatFits
ScrollView {
ForEach(item) { item in
HStack {
Image(item.image)
Text(item.label)
}
}
}
ScrollView {
ForEach(item) { item in
VStack {
Image(item.image)
Text(item.label)
}
}
}
}
The way my views are constructed, it wasn't as simple as this. It would've been tricky to refactor things to work like this. So I came up with a solution to use an Environment value.
enum Layout {
case horizontal, vertical
}
private struct LayoutKey: EnvironmentKey {
static let defaultValue = Layout.horizontal
}
extension EnvironmentValues {
var layout: Layout {
get { self[LayoutKey.self] }
set { self[LayoutKey.self] = newValue }
}
}
ViewThatFits {
Main()
.environment(\.layout, .horizontal)
Main()
.environment(\.layout, .vertical)
}
In this case, the ScrollView is a few views deep in Main
. Each repeated item is a few views deeper. But, at the point I need to decide which layout to use, I can grab my layout Environment value.
So far, this seems to work the way I want! I'm not totally sure if this is performant or not.
I was really glad I was able to get ViewThatFits to work. The only other alternative was changing layouts at some arbitrary sizeCategory
. That would be gross because it would depend on screen size and all that.
SO hopefully this will continue to work well. I should probably rewatch the WWDC video about ViewThatFits.
P.S. In places I couldn't use the Xcode Previews, I've been using Sim Genie to easily change Dynamic Type in the simulator. Fantastic app.