Devlog: June 24, 2023

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 TypeScreen 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.



Date
June 24, 2023