Finally got a couple things done on ScreenCred. Fixed a couple bugs that were…bugging me.
I updated the search sorting a bit. I was finding TMDb’s sorting of results a bit odd. I previously updated it to sort by popularity. Now, if a title matches the query exactly, it will be at the top. I also prioritize matches that contain the exact text of the query. Other than that, still sorts by priority.
I was also having a bug where the toolbar would think the keyboard was still there after dismissing search. This only happened on device, not simulator. For now I put in a little hack to dismiss the keyboard manually before dismissing the sheet. Seems to be related to using .searchable, which does not give access to the focus state of the search field—as far as I know.
With those 2 pesky bugs off my plate, I think I will start on iPad support next week.
I’ve lost my groove. Been a busy week, so I haven’t been able to work on anything. I was going to work on things today, but then some urgent things came up at my day job.
But honestly, not the worst thing. I need a little bit of time to breathe, figure out my priorities, and do a bit of planning.
Speaking of planning, I’ve been trying a new system: index cards. I was getting overwhelmed with task apps. It’s so easy to just put a bunch of things in and have a never ending list. So, now, I’m trying index cards. When I’m planning a feature, I make sure everything fits onto an index card. If it doesn’t fit, it needs to be rethought, reprioritized, or broken up. This helps me make sure my backlog doesn’t get too long. Physically limiting tasks also helps prevent scope creep—it better be important if it’s going to take up space on this card. It seems to be a good system so far. I’m going to keep trying it out and tweaking it.
Got too many things going on right now. Trying to do some redesigns to ScreenCred, but not feeling very inspired. So thought I’d switch to trying and making my own node server to generate the og:image. Netlify has been taking like 10 seconds, which is not great. I think what I came up with should run around ~600ms. Which is still not as fast as I would like, but better than 10 seconds! I’m trying to deploy it to fly.io, but that relies pretty heavily on Docker, and I am not even a novice when it comes to Docker. So maybe I should just run in on my Linode. IDK.
I woke up around 2am and couldn’t sleep. So decided to finish up some work on adding an App Clip to ScreenCred. Since the app is fairly limited in functionality, pretty much all of it can be included in the App Clip. I wanted to do this to go along with sharing links. If you share a link with someone who does not have the app installed, they should see the App Clip card in messages.
The docs for App Clips are actually pretty good. You have to click around a lot to find everything, but I was able to find 99% of what I needed in the official docs. I think I have this all setup correctly—and I’ve tested it as best I can on TestFlight—but kinda seems like you can’t be 100% sure until the app is released.
But, I think it’s mostly working. The cards in iMessage now have an “open” button on them!
Pretty button!
The App Clip will respond to the same URLs that I setup for universal links. The main difference between the app and the App Clip is how they handle the URL. In SwiftUI, the app uses .onOpenUrl, but the App Clip uses .onContinueUserActivity:
It took me a bit to find that NSUserActivityTypeBrowsingWeb is what you want to pass in as the string1.
I’ve only seen one or two App Clips out in the wild, so not totally sure how they are supposed to work. I guess I could read some human interface guidelines and whatnot, but I kinda feel like wingin’ it.
I think I need to give my brain a break for a few days. I’ve been really excited about working on ScreenCred, but it’s been taking over a bit. Need to slow down a bit. I just get so obsessive when I’m learning new technologies like universal links and App Clips, that it’s hard for me to stop until it’s all done. The goal of this is to learn how to make an app in a sustainable way, and I’m starting to see the signs that I’m moving too quickly.
I went down the rabbit-hole. I got an idea in my head that I want my app to have a share feature. Send a link to a friend, and they can open it in the app and load the results automatically. I’ve never done anything like that, so seemed like a cool opportunity to try something new. It got a bit more involved than I originally thought…
Universal Links
The first step was associating my domain with the iOS app. I spun up a simple 11ty site and created ./well-known/apple-app-site-association. It’s a JSON file that defines which urls your app responds to. Right now, I only want one:
{
"applinks": {
"details": [
{
"appIDs": [
"TEAMID.app.bundle.id",
"TEAMID.app.bundle.id.dev"
],
"components": [
{
"/": "/search/?*",
"comment": "Matches any URL with a path that starts with /search/someid."
}
]
}
]
}
}
I technically have two apps—one for local development with .dev on the bundle id, and the other real one. So it’s nice you can associate multiple bundle ids. What to put in this file is not well documented, and Apple does not have a tool to validate it. I did find this validator.
I eventually got all this working, but had quite a bit of trouble.
Universal Links Gotchas
Make sure your JSON is formatted correctly 🤦🏻♂️
Make sure the Content-Type is application/json
I’m not 100% sure this is necessary, but the validator I was using called it out. I needed to add a config in my Netlify config to make this file have the correct content type since it does not have a file type.
Apple caches apple-app-site-association. So changes may not be immediate. Seems to update about once and hour?
To bypass the cache when developing, add ?mode=developer to your url under Associated Domains: applinks:example.com?mode=developer.
But, don’t leave ?mode=developer in for TestFlight or AppStore versions.
This got me to the point that my universal links opened the app in the simulator! That was pretty exciting! To get them to work on an actual device, you need to open Settings > Developer and enabled Associated Domains Development under Universal Links. There’s also a diagnostics that you put in your domain and it will tell you if you have an app installed that will open links from that domain. It works when the dev app is installed, but for some reason, not for TestFlight versions 🤷🏻♂️.
Opening the Link in the App
In SwiftUI, you just have to use .onOpenUrl. Since I’m only handling one url, the logic is simple. I check that the URL has a valid search request, and then set it up in the app. As part of this, I used a RegexBuilder! I didn’t have to, but wanted to try it out. Pro tip, you need to import RegexBuilder.
import RegexBuilder
...
let regex = Regex {
TryCapture {
ChoiceOf {
"m""t"
}
} transform: {
MediaType.fromSearchType(String($0))
}
Capture (
OneOrMore(.digit)
)
}
let matches = url.path().matches(of: regex)
guard matches.count == 2else { return }
let (_, firstType, firstId) = matches[0].output
let (_, secondType, secondId) = matches[1].output
// Fetch data with this information
Generating Landing Page
Now, what if someone does not have the app installed? They should be taken to a somewhat helpful webpage. This part would be pretty simple if I had a server-rendered page, or even a single-page app. But I make things hard for myself and I don’t have that. I have a statically generated 11ty site.
My search urls have the path /search/t123456m7890. That last part is the type and id of 2 movies or shows. That can be anything. I can’t reasonably generate a webpage for each possible combination ahead of time. Enter Netlify On-Demand Builders.
11ty has support for this. So after several attempts, I was able to make a builder function that generates a page with different content based on the last part of that URL.
I got the page to generate, but I wasn’t sure how to get the data dynamically. I’m not an 11ty expert, but the whole point is to have a data ahead of time. I don’t have that. This article cleverly uses a async filter to fetch the data. So pretty much the same thing as the iOS app, I take the search param, validate, parse, and fetch the data. Using that data, I can display the poster images and names of the selected movies or shows.
I was moving fast, I'll make this page look better later. I promise.
Awesome! Safari will also show that button to open it in the app. That’s pretty cool. But, when you share a link in messages, you just get a boring link. It needs some pizazz.
Generating Og:image
iMessage uses the meta og:image to add an image to a link. So since these pages are dynamic, I need to also dynamically generate the og:image as well.
I mostly followed this guide on how to do exactly that with Netlify functions. It worked perfectly locally, but when I deployed to Netlify, I had a lot of issues. Basically it came down to using @sparticuz/chromium instead of chrome-aws-lambda. It’s beyond me what the differences are, but came across the solution on the Netlify support forums.
Aside from those issues, it’s fairly simple. Create and HTML file with some CSS. I did have to change the page.setContent to waitUntil: "networkidle0" instead of "domcontentloaded". I’m sure that makes it run longer, but the poster images wouldn’t load otherwise.
I still have some wrinkles to iron out. I guess the pages don’t always generate quickly enough or something because when you open the share sheet, you don’t always see the og:image…but sometimes you do…so I’m not sure. I’ll see what I can do.
I guess at this point it’s a little hard to hide the fact that the name of the App is ScreenCred.
Anyway, that was my adventure of the past couple of days. Was pretty fun and I hope it all keeps working!
Today was a bit of a chores day. I’m working on a Swift package to hold all the common extension, helpers, components etc. between my apps. Cleverly named SamKit. I added SamKit, dealt with issues like making things public and whatnot. I’ve added it as a local package so that I can edit it along side my app.
I’m also getting things ready for TestFlight 😱. It will be a bit before it’s ready for public testing—want to get some webpages ready first. I was a bit frustrated because apparently the name I picked (and already bought a domain for) is taken. I had done an App Store search and didn’t find anything the night I was coming up with names. Maybe I missed it. But it’s there now. So I had to spend about 20 mins coming up with a name that would fit in the 30 character limit1. Hopefully the other app will be just go away one of these days and I can take the short name2.
I’m giving Xcode Cloud a shot again, but honestly, not very impressed at the moment. Queue times seem to be really long. Like 20mins. Was much faster when I was beta testing it. So maybe I’ll switch to something like Fastlane and do it locally instead.
What’s the name you ask? Patience. I will let you all know soon.↩︎
It hasn’t been updated in over a year and has 1 star.↩︎