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.

Screen recording of ScreenCred showing the layout changing when using large Dynamic Type

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.