Today’s feature is sorting. Initially, I hardcoded it to sort by number of credits, most to least. Thought it might be nice to let you wort the other way, and by name. The trickiest part of figuring out what UI to have for option. Right now, the best place is in my “more” menu. I went through a couple iterations though.
First
Wall sort options
This was my first take. My instinct is to lay all the sort options out so they are easily accessible. Generally, I think this is a good idea. But here, it felt too repetitive, cluttered and, in my opinion, didn’t give enough information of what the sort actually was. Maybe if I could think of better labels and icons, it would work better.
Second
Looking for inspiration, I came across the sort options in iA Writer.
iA Writer is a good app and we should emulate good apps right?
I like that it divides what you can sort by from the direct. This also lets you give really descriptive labels for the sort directions 👍1. So I went in that direction.
Look familiar?
I like it! But, it has one crucial flaw—changing what you sort by immediately closes the menu, so you have to open it up again to change the direction. Not a great experience.
Third
Okay. Too many taps. Can I make it less taps? What about more nested menus? Everybody likes that right?
Better?
You have to really drill in, but you can get exactly the sort you want without too much fuss. Which I like better.
Final?
Then it hit me, why am I putting the options under a submenu. So I put them in their own section at the top level of the menu. This is my favorite so far and what I’m sticking with. Lets me keep the descriptive sort direction labels, doesn’t take up too much space, and doesn’t require too many taps. Plus it has a cute like section header.
Just right...hopefully
I didn’t know you could put a section header in a menu. Here’s the code if it’s helpful at all.
Menu {
Section("Sort") {
Menu {
Picker("Number of Credits", selection: $creditSort) {
Text("Most to Least")
.tag(CreditSort.numCredits(.descending))
Text("Least to Most")
.tag(CreditSort.numCredits(.ascending))
}
} label: {
ifcase .numCredits(_) = creditSort {
Label("Number of Credits", systemImage: "checkmark.circle")
} else {
Text("Number of Credits")
}
}
Menu {
Picker("Number of Credits", selection: $creditSort) {
Text("A to Z")
.tag(CreditSort.name(.ascending))
Text("Z to A")
.tag(CreditSort.name(.descending))
}
} label: {
ifcase .name(_) = creditSort {
Label("Name", systemImage: "checkmark.circle")
} else {
Text("Name")
}
}
}
}
This might not be the simplest way of doing this, but it’s straightforward, at least to me. Had to do some trickery to add a checkmark to the menu item to show a child is selected. Might clean that up later.
It was fun to iterate on this and figure out all the different ways I could do this. There’s a bunch of options I didn’t explore at all. Going to let this sit for a few days and see how it feels.
In general, ascending and descending mean almost nothing to be when it comes to sorting. I like it when it’s clear—A to Z, Z to A, etc.↩︎
My family has been sick for a bit, so I’ve been mostly working on taking care of them and trying to not get sick myself. Finally starting to get back to a bit of normalcy now.
Today I added a button.
🥳
It’s a details button next to a name. It will open the details of that person in The Movie Database. I don’t want to make a full TMDb client. So my current plan is to just link to their pages. A pretty straightforward
I don't like where the button is...
Along with that, I added a setting to open the details in the app or in Safari. The in-app browser uses SFSafariViewController. This is the one that comes with the done button, the open in Safari button, etc. I first tried WKWebView but that just displays a webpage with none of the other stuff. When the setting is enabled to open details in Safari, instead of using Link, I use openUrl environment variable. Pretty nifty.
structDetailsLink: View{
@AppStorage(SettingsKeys.openDetailsInBrowser) var openDetailsInBrowser = false
@Environment(\.openURL) var openUrl
var body: someView {
Button {
if openDetailsInBrowser {
openUrl(url)
} else {
// Show
}
} label: {
Label("Details", systemImage: "info.circle")
}
}
}
I’m using @AppStorage for saving the setting. It is simple, but I’m not 100% sold on it yet. It requires a lot of duplication. If I want to use the setting in multiple places, I need to define a default in multiple places, which feels odd to me. Just found this article about an alternative that looks interesting. Going to give that a closer read.
I’ll be brief because kids are crying and we need to eat dinner soon.
I did a small revamp of history. I was storing each time a comparison ran, even if the two movies or shows were exactly the same. So first, I added a toggle to hide duplicates. Then I started on deletion. I wanted to show all items when you went to delete them. It started to get a bit unwieldy. So I removed that setting. Back to seeing all your comparisons. Then I decided, instead of adding a new entry each time, if there is an existing one, update the history item. Maybe I can add a counter for the number of times those two were compared or something.
Anyway, came full circle a couple times, but happy where I landed.
Did have a strange issue with EditMode where the environment was not being set how I would expect. I ended up having to declare my own EditMode state and then pass that as an environment to EditButton and my List. Otherwise, my History View had no idea if the list was editing or not.
Went the ZStack route. Things seem to be working well now!
Much better!
I also added the poster images behind the thick material background. I think this gives a cool colored effect when something is selected. I let the material do the magic so I don’t have to determine the dominant colors of an image.
I’m sure I will come across some edge cases I need to deal with, but I’m pretty happy with the effect.
Was kinda in a funk today. Took on a task that was maybe a bit beyond my energy level today. I’m trying to make the images shrink as you scroll up. I have a GeometryReader updating a PreferenceKey as you scroll. The problem is that the size adjustment changes the position of my measurement view in the scroll view. I think this is because my header is in a safeAreaInset. I may need to change my header to be in a ZStack or something.
Not great...
I’m not quite sure what else to try. Using safeAreaInset seems to be the right choice for this layout, but obviously is causing issues. Even if I use the global coordinate space instead of a named/local one, the offset does not seem to change “smoothly”. So probably going to need to try some different layouts.
I don’t know what got into me. Basically, I nerd sniped myself. I thought, “sure would be nice if I could get a notification if it looks like it’s going to freeze tonight.” A somewhat reasonable thought. I could’ve looked for an existing solution, or cobbled together a Shortcut, or use some other use some other automation tool. But no. I had the bright idea to make my own solution and I mostly regret it.
My brain came up with this idea yesterday:
I want a push notification on my phone. (Never done)
Use Apple WeatherKit. (Never used)
Make a server to check the weather and send a push notification to…
Oh yes. Need an iOS app too.
Writing a server? Why not use Go? (Minimal Go experience)
Okay.
What? Why?
All of this made sense in the moment. Using WeatherKit seems like a cool idea. I’ve wanted to give that a try and it has a RESTAPI. Go for the server?? I’ve hardly used Go! I should’ve used Vapor. Better yet, scrap all this and use IFTTT or something. But I was already too far gone. My brain was obsessed and I stayed up to about 2am working on it. It was kinda fun.
The result is a small Go server that should hopefully check the weather at 7pm and send me a push notification if the temperature looks like it will be close to freezing.
The iOS app is literally the SwiftUI starter that requests notification permission when it launches. I archived it and picked ad-hoc distribution and installed it though the device manager in Xcode. Never done that before either. That’s kinda cool.
One little tidbit was how to get the correct string representation of the device token. It’s given to you as Data but it needs to be the hex values. I tried .base64EncodedString() but that did not work. Some googling found this: