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.

Projects ScreenCred devlog

Devlog: June 21, 2023

Not sure what happened, but I was no longer able to access https://screencred.app. I’m hosting it on fly.io, and using Cloudflare in front of it. Seemed like the fly.io certificate expired.

The trick seemed to be that I needed to make sure my _acme-challenge CNAME record is unproxied, but keep the A and AAAA records proxied in Cloudflare. I now see that the fly.io certificate has been renewed. So hopefully that’s all it is.

devlog

Devlog: June 15, 2023

Quick report. Been going through ScreenCred with Voice Over and on an iPhone 13 mini simulator with Accessibility Extra Extra Extra Large Dynamic Type enabled. So far I have found about 30 things that need to be fixed and improved. Most are small, simple things. Others are going to require new designs at larger text sizes. I’ll get those fixed and then go through it all again.

Projects ScreenCred devlog

Devlog: June 15, 2023

Worked on adding a tip jar to ScreenCred.

I followed this YouTube tutorial which was pretty great.

Screenshot of ScreenCred showing a tip jar view with 4 tip options. $1, $3, $5, and $10.Screenshot of ScreenCred showing a tip jar view with 4 tip options. $1, $3, $5, and $10. Screenshot of a thank you with confetti falling that is shown after a tip is purchased.Screenshot of a thank you with confetti falling that is shown after a tip is purchased.

I went with the new nice round numbers $1.00, $3.00, $5.00, and $10.00. To me, those seem a little more personal for some reason. Went with the generic copy iPhone naming for the tip names.

I thought about writing my own confetti view for the thank you, but found ConfettiSwiftUI. Was simple to add and looks really nice!

The tips are consumable in-app purchases and don’t unlock anything. But, when the purchase is completed, I do store in NSUbiquitousKeyValueStore that a tip was purchased. I’m hoping I can use this in the future to unlock things like alternate app icons or something. Never used it before, so hopefully it works…

With the tip jar done, I’m moving on to going through the app as carefully as I can to make sure it’s as accessible as I can make it, including Dynamic Type sizes. I’m getting close to ScreenCred being at a place where I would be comfortable releasing it. I decided I want to release it before I start working on my other app idea.

Projects ScreenCred devlog

Devlog: June 13, 2023

I’ve heard that friends don’t let friends ship an app without adding a request for reviews. So I added that.

I followed two apple guides—the Human Interface Guidelines and requesting review documentation. I kinda just copied what the docs show—request once per version after at least 4 completed actions, and after the user has paused for 2 seconds.

.onReceive(viewState.$comparison
    .debounce(for: 2.0, scheduler: RunLoop.main)
) { _ in
            let version = AppInfo.fullVersion
            guard viewState.activeSheet == nil
                    && viewState.comparison.first == nil
                    && viewState.comparison.second == nil
                    && version != lastVersionPromptedForReview
                    && searchesCompleted > 3 else { return }
            
            requestReview()
            lastVersionPromptedForReview = version
        }

Here you can see some of the ugly internals of my app. The View I want to request a view on is show based on the state of the current comparison/search. So I debounce that change. Once it has settled for 2 seconds, I check if any sheets are shown, if the search is empty, and if I’ve already request a review this version. Might be better ways, but I’ll give this a shot. I’m not sure if there is a great way to test it…

Next, in the spirit of learning new things, I think I’ll try adding some in app purchases. Just a couple small tips that will unlock alternate icons or something.

Projects ScreenCred devlog

I’m Not Ready for visionOS and Spatial Computing

visionOS and the idea of spatial computing are pretty exciting. But as a mostly hobbyist iOS developer, I’m already a bit exhausted thinking about a new platform.

To make great apps, indie and solo devs already have a long list of skills and APIs they need to excel at. For myself, I’m a little worried about what visionOS and spatial computing is going to add to that list. I’ve started working through the related WWDC videos, and sure, a lot of what you know already will apply to visionOS—especially with SwiftUI. But it seems like to make truly great experiences, you’re probably going to also need to know 3D modeling, how to write custom Metal shaders, really understand ARKit, and more.

I’m sure there are people who are going to be able to keep up and master all this and be incredibly successful. And maybe what Apple provides will make all this way easier than I’m expecting. The baseline of people’s expectations of what an app provides seems to go up and up each year. I’m a little worried if I can keep up. Maybe I don’t need to, but at this moment, it feels a bit daunting.

Of course, this is coming from someone who doesn’t have any apps on the App Store and spends only a couple hours a week on making apps.

Picture of Sam Warnick

As my daughter says, I'm just a tired dad, with a tired name, Sam Warnick. I'm a software developer in Beaufort, SC.

Some things I do