Categories
Programming

FocusedValue on tvOS

Alright, let’s set the scene with a mildly contrived example: you’ve got a list of things, and you want the surrounding View to match them. For a tvOS app, this could be swapping out the background and some title text to match the selected episode; since I don’t feel like coming up with some fake episode titles, however, we’re going to go with colors:

At first thought, you’d probably reach for @State, something akin to:

struct ColorsView: View {
	@State var selectedColor: Color
	let colors: [Color]

	var body: some View {
		VStack {
			Text(colorName(selectedColor))
			HStack {
				ForEach(colors) { color in 
					Rectangle().fill(color).focusable()
				}
			}
		}
			.background(WaveyShape().fill(selectedColor)
	}
}

Not too bad; attach on onFocus to the Rectangle() and it should work!

But… what if there’s more layers in between? Instead of a Rectangle(), you’ve got some other View in there, maybe doing some other logic and stuff.

Oof, now we’re going to need a @Binding, and – oh, what happens if the user swipes out of the rectangles and to our nav bar, can selectedColor be nil?

Happily, SwiftUI has something built out to handle basically this exact scenario: @FocusedValue. There’s some great tutorials out there on how to do this for a macOS app, which allows you to wire up the menu bar to respond to your selection, but it works just as well on tvOS.

Let’s get started:

struct FocusedColorKey: FocusedValueKey {
	typealias Value = Color
}

extension FocusedValues {
	var color: FocusedColorKey.Value? {
		get { self[FocusedColorKey.self] }
		set { self[FocusedSeriesKey.self] = newValue }
	}
}

Now we’ve got our new FocusedValue available, so let’s use it:

struct ColorsView: View {
	@FocusedValue(\.color) var selectedColor: Color?
	
	var body: some View {
		VStack {
			Text(colorName(selectedColor ?? Color.clear))
			HStack {
				ForEach(colors) { 
					ColorRectangleView(color: $0)
				}
			}
		}
			.background(WaveyShape().fill(selectedColor ?? Color.clear)
	}
}

The one big change here is that selectedColor can be nil. I’ve gone ahead and defaulted to .clear, but do what fits your use case.

Finally, we need to set the focused item:

struct ColorRectangleView: View {
	let color: Color

	var body: some View {
		Rectangle()
			.fill(color)
			.focusable()
			.focusedValue(\.color, color)
		}
	}
}

Et voila, it works!

Now, this may not seem like a huge change over doing it via @Binding, but keep in mind: @FocusedValue is a singleton. You can have every view in your app respond to this, without passing @Bindings every which way.

Categories
Playlist

Playlist of the Month: May 2021

If you haven’t gotten a vaccine yet, go do it.

Cologne – Haux on Something to Remember – EP

Slowly – ODIE on Slowly – Single

How It Was – Yoste on A Few Brief Moments – EP

Come On – Will Young on Echoes

We’ll Be Alright – Yoste on A Few Brief Moments – EP

DON’T TELL THE BOYS – Petey on Checkin’ Up on Buds – EP

Spaces – Jaymes Young on Spaces – Single

FOR THE REST OF MY LIFE – Zack Villere on FOR THE REST OF MY LIFE

Parachute (Piano Sessions) – Seafret on Parachute (Piano Sessions) – Single

Arcade – Duncan Laurence on Arcade – Single

twentyfive – Yoste on twentyfive – Single

Lightning – Hayden Calnin on Lightning – Single

Oh Dear, Oh Beaux – beaux on A Love Letter To the Moments Spent Outside

skip.that.party – X Ambassadors & Jensen McRae on skip.that.party – Single

I’m God – Clams Casino & Imogen Heap on I’m God – Single

Lost In the World (feat. Bon Iver) – Kanye West on My Beautiful Dark Twisted Fantasy

I Can’t Lose You – Isak Danielson on Tomorrow Never Came

Broken Heart Gang – Teflon Sega on Broken Heart Gang – Single

Losing My Religion – Shawn James on The Lake Wenatchee Sessions – EP

MONTERO (Call Me By Your Name) – Lil Nas X on MONTERO (Call Me By Your Name) – Single

Science/Visions – CHVRCHES on In Search of Darkness – EP

House of Balloons / Glass Table Girls (Original) – The Weeknd on House of Balloons (Original)

On My Own (feat. Kid Cudi) – Jaden on ERYS

Neon Medusa – The Midnight on Horror Show – EP

We Don’t Talk Enough – Quinn XCII & Alexander 23 on Change of Scenery II

We Got Gods to Blame – Hayden Calnin on We Got Gods to Blame – Single

Flow – Vide on Flow – Single

Into Your Arms – Vide on Into Your Arms – Single

Starlite – HOKO on Heathen

DIM – SYML on DIM – EP

Blinding Lights – X Ambassadors on Blinding Lights – Single

Raining – daydream Masi on Movie Scenes EP

BLACK TEETH – SYML on DIM – EP1

Good Graces – Spencer William on Little Wars – EP

Only Pieces of the Truth – Jordan Hart on Only Pieces of the Truth

All the Lights – By The Coast on All the Lights – Single

I Hate You For This – Munn on I Hate You For This – Single

CRYSTAL BALL – Jake Miller on CRYSTAL BALL – Single

Magic – Phillip LaRue on Night Swimming – Single

I Took a Pill in Ibiza, Youth (Acoustic Mashup) [feat. Chasing Taylor] – Landon Austin on Covers: Six

Overtime – Griffin Stoller on Overtime – Single

From the Back of a Cab – Rostam on Changephobia2

Always – Isak Danielson on Yours

So Handsome Hello – Woodkid on Powerplant – EP

Young Blood – PHOEBE ∆X∆ on Wisdom Teeth, Pt. 1 – EP

Astronaut In The Ocean – Masked Wolf on Astronaut In The Ocean – Single

Rät – Penelope Scott on Public Void3

Peach Scone – Hobo Johnson on Peach Scone – Single4

Fashion – Emilee Moore on Fashion – Single

Golden Days – ferdinant. on Golden Days – Single

Follow – Vide on Follow – Single

Empty Space – Teflon Sega on Empty Space – Single

Because the Night (feat. Nikki Flores) – The Midnight on Horror Show – EP

Good in Red – The Midnight on Horror Show – EP5

Klan – Mahmood & DRD on Klan – Single

Somewhere We Can Be Alone – Gunnar Gehl on Somewhere We Can Be Alone – Single

Symphony – Forester on Symphony – Single

The Stranger – The Midnight on Horror Show – EP

Shadows in the Dark (feat. Elliot Moss) – HNTR on Shadows in the Dark (feat. Elliot Moss) – Single

Devil Make a Deal – The Midnight on Horror Show – EP

Ghost in Your Stereo – The Midnight on Horror Show – EP

Hallucinogenics (Vallis Alps Remix) – Matt Maeson on The Remixes – EP

can’t give back your love – Clide on can’t give back your love – Single

Stay a Little Longer (feat. Vide) – LANDR & Nander on Stay a Little Longer (feat. Vide) – Single

Bite Marks – Au/Ra on Bite Marks – Single

If I Cared – Jack in Water on If I Cared – Single

start//end – EDEN on vertigo

Coldplay (feat. Vic Mensa) – Mr Hudson on Coldplay (feat. Vic Mensa) – Single

Save Me – Majik on It’s Alright / Save Me – Single6

  1. “You were an honest mistake.” oof.
  2. I also quite like the music video, it’s a nice counterpart to the song.
  3. “It tasted like Thomas Malthus.” is a truly excellent line. Really, all the lyrics in here are great.
  4. This whole thing feels like he just hit record and did the entire vocal track in one go, and then filled in the music afterwards. Very improvisational feel.
  5. Really enjoying this whole EP, in case you can’t tell by the way I’ve got the entire thing on this playlist.
  6. I miss Majik so much, and I hope the guys that used to be the band are doing well.
Categories
Programming

Position vs Offset

I’ve had reason recently to be doing custom position of things in SwiftUI, and figured I’d share something that I found a bit of a tricky distinction at first: position vs offset.

So, let’s set the scene:

struct DemoView: View {
	var body: some View {
		HStack {
			Rectangle().fill(.gray)
			Rectangle().fill(.blue)
			Rectangle().fill(.gray)
		}
	}
}

And now I’ll add some visuals, for ease of reading. (I made these in Sketch, so the graphics aren’t precisely what you’d get by running this code, but hey, artistic liberties.) Here’s our view:

Three squares in a row.

Now let’s tinker!

struct DemoView: View {
	var body: some View {
		HStack {
			Rectangle().fill(.gray)
			Rectangle().fill(.blue)
				.offset(x: 150, y:150)
			Rectangle().fill(.gray)
		}
	}
}

We’re offsetting the middle square. What does that look like?

Three squares; two are in a row, and there is a space in the middle where the third could fit, but it is below and to the right of that space.

I’ve left in a little ghost image to show where it was, because it’s an important distinction! As far as the HStack is concerned, that space is still occupied. Think of it like throwing your voice – you don’t move, just the perception of where you are.

Let’s try something else, now:

struct DemoView: View {
	var body: some View {
		HStack {
			Rectangle().fill(.gray)
			Rectangle().fill(.blue)
				.position(x: 150, y:150)
			Rectangle().fill(.gray)
		}
	}
}

Looks pretty similar in code, right? We’ve just swapped out ‘offset’ for ‘position’. What do we get on screen?

Three squares; two are in a row, while the third is out of alignment and slightly overlapping.

Ooh, very different! No more ghost, because now it’s actually in a different place – not holding that spot in the HStack. It’s also in a different spot than the previous one, what gives?

It’s in the name: ‘offset’ offsets the view from where it normally would’ve been. Our starting position was where the ghost stayed:

Three squares; two are in a row, and there is a space in the middle where the third could fit, but it is below and to the right of that space. The distance from the space to the square is labeled, with 150 in the top distance and 150 in the left distance.

‘Position,’ on the other hand, skips the whole question of where it would go and instead just puts it in an exact spot, using the top left corner of the screen as the point (0,0):

Three squares; two are in a row, while the third is out of alignment and slightly overlapping. The distance from the misaligned square to the top and left of the image are labeled with '150' on each.

The other approach that worked for my brain, coming from doing a lot of web dev, is to think about ‘offset’ as being CSS’ ‘position: relative’, while ‘position’ is equivalent to ‘position: absolute’.

Hopefully this helps the whole thing make sense!

Categories
Programming

Transitions in ZStacks

The other day, I found a fun little SwiftUI edge case: using a .transition() modifier in a ZStack doesn’t work. As a basic demo, let’s do something like this:

struct DemoView: View {
	@State var showing = true
	
	var body: some View {
		ZStack {
			Rectangle().fill(Color.red)
				.onTap {
					showing.toggle()
				}
			if (showing) {
				Text("Hello, world!")
					.transition(.opacity)
			}
		}
	}
}

Pretty simple example, yeah? Seems like it’d Just Work(TM) out of the box. It doesn’t, though; instead of a lovely opacity animation, it just pops in and out. Hmm.

Worry not, though, as I have a fix:

struct DemoView: View {
	@State var showing = true
	
	var body: some View {
		ZStack {
			Rectangle().fill(Color.red)
				.onTap {
					showing.toggle()
				}
				.zIndex(1)
			if (showing) {
				Text("Hello, world!")
					.transition(.opacity)
					.zIndex(2)
			}
		}
	}
}

Et voila, your transition works!

Now, I haven’t exactly done rigorous experimentation to figure out why, exactly, this works, but I do have a theory: when SwiftUI removes the Text, it loses its place in the ZStack, and gets assigned to “behind everything else.” By manually specifying the order, the slot stays reserved, and it can stay there while it fades in and out.

Categories
Programming

Playing Videos in SwiftUI

As of WWDC 2020, we have a way to play videos in SwiftUI without bailing out to UIKit with UIViewRepresentable. At first glance, it’s pretty simple, as well:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL

	var body: some View {
		let player = AVPlayer(url: videoURL)
		VideoPlayer(player: player)
	}
}

Et voila, you’re playing a video! You can overlay content on top of the video player pretty easily:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL

	var body: some View {
		let player = AVPlayer(url: videoURL)
		VideoPlayer(player: player) {
			Text("Watermark")
		}
	}
}

Seems like we’re good to go, no?

Well, not quite. Let’s talk memory management.

VideoPlayerView is a struct – it’s immutable. SwiftUI allows us to mutate the state of our views with user interaction using things like @State, thanks to some Compiler Magic.

Every time some aspect of the state changes, SwiftUI calls the body getter again.

Spotted the catch yet?

We’re declaring the AVPlayer instance inside the body getter. That means it gets reinitalized every time body gets called. Not the best for something that’s streaming a video file over a network.

But wait, we’ve already mentioned the Compiler Magic we can use to persist state: @State! Let’s try:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL
	@State var player = AVPlayer(url: videoURL)

	var body: some View {
		VideoPlayer(player: player)
	}
}

Whoops. We’ve got a problem – self isn’t available during initialization, so we can’t initialize the AVPlayer like that. Alright, we’ll write our own init:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL
	@State var player: AVPlayer

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = State(initialValue: AVPlayer(url: videoURL))
	}

	var body: some View {
		VideoPlayer(player: player)
	}
}

(I suppose we could drop the let videoURL: URL there, since we’re using it immediately instead of needing to store it, but for consistency’s sake I’m leaving it in.)

Okay, sounds good – except, hang on, @State is only intended for use with structs, and if we peek at AVPlayer it’s a class.

Okay, no worries, that’s what @StateObject is for, one more tweak:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL
	@StateObject var player: AVPlayer

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = StateObject(wrappedValue: AVPlayer(url: videoURL))
	}

	var body: some View {
		VideoPlayer(player: player)
	}
}

There, we should be good to go now, right? Right?

Alas, the compiler says no. AVPlayer doesn’t conform to ObservableObject, so we’re out of luck.

Fortunately, ObservableObject is pretty easy to conform to, and we can make our own wrapper.

import SwiftUI
import AVKit
import Combine

class PlayerHolder: ObservableObject {
	let player: AVPlayer
	init(videoURL: URL) {
		player = AVPlayer(url: videoURL)
	}
}

struct VideoPlayerView: View {
	let videoURL: URL
	@StateObject var playerHolder: PlayerHolder

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = StateObject(wrappedValue: PlayerHolder(videoURL: videoURL))
	}

	var body: some View {
		VideoPlayer(player: playerHolder.player)
	}
}

Phew. At long last, we’ve got a stable way to hold onto a single AVPlayer instance. And, as a bonus, we can do stuff with that reference:

import SwiftUI
import AVKit
import Combine

class PlayerHolder: ObservableObject {
	let player: AVPlayer
	init(videoURL: URL) {
		player = AVPlayer(url: videoURL)
	}
}

struct VideoPlayerView: View {
	let videoURL: URL
	@StateObject var playerHolder: PlayerHolder

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = StateObject(wrappedValue: PlayerHolder(videoURL: videoURL))
	}

	var body: some View {
		VideoPlayer(player: playerHolder.player)
			.onAppear {
				playerHolder.player.play()
			}
	}
}

Will start playing the video as soon as the view opens. Similarly, you could add some Buttons, build your own UI on top of the video player, all sorts of fun.

And, from the other end, you can put more logic in the PlayerHolder, as well. Say you need some additional logic to get from a video ID to the actual URL that the AVPlayer can handle? Try something like this:

class PlayerHolder: ObservableObject {
	@Published var player: AVPlayer? = nil
	init(videoID: Video.ID) {
		NetworkLayer.shared.lookupVideoInformation(videoID) { result in 
			self.player = AVPlayer(url: result.url)
		}
	}
}

(Now, that’s not the nice, Combine-y way to do it, but I’ll leave that as an exercise for the reader. Or possibly another post.)

Categories
Programming

tvOS Carousels in SwiftUI

It’s a fairly common pattern in tvOS apps to have a carousel of items that scrolls off screen in either direction – something vaguely like this:

Image a tvOS wireframe, showing two horizontally-scrolling carousels.

Actually implementing this in SwiftUI seems like it’d be easy to do at first:

VStack {
	Text("Section Title")
	ScrollView(.horizontal) { 
		HStack {
			ForEach(items) { 
				ItemCell(item: $0)
			}
		}
	}
}

Which gets you… a reasonable amount of the way there, but misses something: ScrollView clips the contents, and you wind up looking like this:

A tvOS wireframe, showing two horizontally-scrolling carousels; both have been clipped to the wrong visual size.

Not ideal. So, what’s the fix? Padding! Padding, and ignoring safe areas.

VStack {
	Text("Section Title").padding(.horizontal, 64)
	ScrollView(.horizontal) {
		HStack {
			ForEach(items) { 
				ItemCell(item: $0)
			}
		}
			.padding(64) // allows space for 'hover' effect
			.padding(.horizontal, 128)
	}
		.padding(-64)
}
	.edgesIgnoringSafeArea(.horizontal)

The edgesIgnoringSafeArea allows the ScrollView to expand out to the actual edges of the screen, instead of staying within the (generous) safe areas of tvOS.1

That done, we put the horizontal padding back in on the contents themselves, so that land roughly where we want them. (I’m using 128 as a guess; your numbers may vary, based on the design spec; if you want it to look like The Default, you can read pull the safe area insets off UIWindow.)

Finally, we balance padding on the HStack with negative padding on the ScrollView; this provides enough space for the ‘lift’ (and drop shadow, if you’re using it) within the ScrollView, while keeping everything at the same visual size.

  1. tvOS has large safe areas because TVs are a mess in regards to useable screen area.
Categories
Playlist

Playlist of the Month: April 2021

Very nearly didn’t get this written up on time; I had a rough time of it with the vaccine side effects, but I’m thankfully through it now. And vaccinated! Get a vaccine, folks – a day of feeling like crap is better than getting covid, and better than giving someone else covid.

Cologne – Haux on Something to Remember – EP

Somewhere (feat. Octavian) – The Blaze on Somewhere (feat. Octavian) – Single

Slowly – ODIE on Slowly – Single

How It Was – Yoste on A Few Brief Moments – EP

Come On – Will Young on Echoes

We’ll Be Alright – Yoste on A Few Brief Moments – EP

DON’T TELL THE BOYS – Petey on Checkin’ Up on Buds – EP

Spaces – Jaymes Young on Spaces – Single

FOR THE REST OF MY LIFE – Zack Villere on FOR THE REST OF MY LIFE

Nuke the Moon – Epic Mountain on Nuke the Moon – Single

Crash Into Me – Petey on Crash Into Me – Single

The Light – Richard Walters on The Light – Single

Parachute (Piano Sessions) – Seafret on Parachute (Piano Sessions) – Single

Lo Vas A Olvidar – Billie Eilish & ROSALÍA on Lo Vas A Olvidar – Single

Cold Sets In – World’s First Cinema on Cold Sets In – Single

Arcade – Duncan Laurence on Arcade – Single1

I’m Worried About You – Addict., REWiND & Achex on I’m Worried About You – Single

Red Run Cold – World’s First Cinema on Red Run Cold – Single

twentyfive – Yoste on twentyfive – Single

emily – Jeremy Zucker & Chelsea Cutler on brent ii – EP

fan behavior – Isaac Dunbar on evil twin

Lightning – Hayden Calnin on Lightning – Single

Oh Dear, Oh Beaux – beaux on A Love Letter To the Moments Spent Outside

skip.that.party – X Ambassadors & Jensen McRae on skip.that.party – Single

im a bad friend – Andy H on im a bad friend – Single

Night Like This – daydream Masi on Movie Scenes EP

Hands On The Devil – Smeyeul. & Galvanic on Hands On The Devil – Single

Calamity Song – The Decemberists on The King Is Dead

Whatever You Like – Anya Marina on Whatever You Like [Digital 45]

Hide & Seek (Imogen Heap Cover) – Amber Run on **

I’m God – Clams Casino & Imogen Heap on I’m God – Single

Lost In the World (feat. Bon Iver) – Kanye West on My Beautiful Dark Twisted Fantasy2

I Can’t Lose You – Isak Danielson on Tomorrow Never Came

There for You – Daniel Allan on There for You – Single

RLNDT – Bad Bunny on X 100PRE

Rohypnol (feat. Fixway & Samer) – APRE on 19

Video Games – Black Match on Basement Covers I – Single

Blindside – Nathan Ball on Blindside – Single

Float On – Phil Good on Float On – Single

Broken Heart Gang – Teflon Sega on Broken Heart Gang – Single

Losing My Religion – Shawn James on The Lake Wenatchee Sessions – EP

Wicked Game (feat. Cal Trask) – StayLoose on Wicked Game (feat. Cal Trask) – Single

Wicked Games (Original) – The Weeknd on House of Balloons (Original)3

Can I Be The One? – daydream Masi on Movie Scenes EP

Death Stranding – CHVRCHES on In Search of Darkness – EP

Long Distance – JORDY on Long Distance – EP

Empty Eyes – Munn on Empty Eyes – Single

As I Am (feat. Khalid) – Justin Bieber on Justice (Triple Chucks Deluxe / Deluxe Video Version)

Human – Peter Thomas on Let It All Happen

MONTERO (Call Me By Your Name) – Lil Nas X on MONTERO (Call Me By Your Name) – Single4

Resolve – Hayden Everett on Resolve – Single

Tether – CHVRCHES on In Search of Darkness – EP

Science/Visions – CHVRCHES on In Search of Darkness – EP

carpool – Zachary Knowles on carpool – Single

House of Balloons / Glass Table Girls (Original) – The Weeknd on House of Balloons (Original)

It’s Raining, It’s Pouring – Anson Seabra on It’s Raining, It’s Pouring – Single

BERNADETTE (feat. Joyce Wrice) – Zack Villere on BERNADETTE (feat. Joyce Wrice) – Single

On My Own (feat. Kid Cudi) – Jaden on ERYS5

Feeling Whitney – Post Malone on Stoney (Deluxe)

11H30 – Danger on 09/14 2007 – EP

Broken Bones – CHVRCHES on In Search of Darkness – EP

Peaches (feat. Daniel Caesar & GIVĒON) – Justin Bieber on Justice (Triple Chucks Deluxe / Deluxe Video Version)

Addicted – Blakey on Addicted – Single

Just Fine – Joel Ansett on Just Fine – Single

Destroyer – Of Monsters and Men on Destroyer – Single6

22Hrs – Teflon Sega & OZZIE on 22Hrs – Single7

Vincent Price – HCK9 on Vincent Price – Single

Hollow – Jon Bryant on Hollow – Single

Neon Medusa – The Midnight on Horror Show – EP8

Holding Back (feat. Benjamin Yellowitz) – DYVR on Holding Back (feat. Benjamin Yellowitz) – Single

We Don’t Talk Enough – Quinn XCII & Alexander 23 on Change of Scenery II

Change For the Better – Star Seed & Suave on Change For the Better – Single

9 Crimes – Damien Rice on 9 [Explicit]

Ride It – Regard on Ride It – Single

We Got Gods to Blame – Hayden Calnin on We Got Gods to Blame – Single

Schemers – Steve Benjamins on Schemers – Single

Flow – Vide on Flow – Single

Can You Hear Me? – Munn on Can You Hear Me? – Single

Into Your Arms – Vide on Into Your Arms – Single

Lies – Sloane & Cobe Jones on Lies – Single

Envidioso – Ozuna & Ovi on Envidioso – Single

Starlite – HOKO on Heathen9

Only Human – Woodlock on The Future of an End

Anxious Smothers – Jack in Water on Anxious Smothers – Single

Devil Knows – Armen Paul on Devil Knows – Single

ADDERALL – Jake Miller on ADDERALL – Single

DIM – SYML on DIM – EP

Blinding Lights – X Ambassadors on Blinding Lights – Single10

Raining – daydream Masi on Movie Scenes EP

BLACK TEETH – SYML on DIM – EP

Good Graces – Spencer William on Little Wars – EP

Something Just Like This – The Chainsmokers & Coldplay on Something Just Like This

  1. Really fun to sing along to, but I keep winding up in the wrong key if I try to sing it without the music. Not sure what’s going on there.
  2. One day I want to arrange an a cappella cover of this, because it just seems like it’d be a fun challenge.
  3. Still amused by the coincidence of these two being in a row.
  4. If you haven’t watched the video yet, you really should. It’s great.
  5. Hey, that new Spider-Man game is fun! Really suffers from the very-overdone “Spider-Man can’t let his loved ones find out he’s Spider-Man” thing, but still, fun gameplay.
  6. Apparently “Of Monsters and Men” is my Moving Soundtrack?
  7. Realized as I was putting this playlist together that I’ve been misreading the artist as “Teflon Saga” this whole time.
  8. Very ‘80s vibes to this one, it’s quite fun.
  9. Favorite addition this month.
  10. A nice acoustic kind of cover.
Categories
Review

“How to Avoid a Climate Disaster”

Bill Gates

I just have to begin by expressing my admiration for Bill Gates. Which still feels strange – I grew up on “Micro$oft” jokes and the image of Gates as the corporate Big Brother, a la Apple’s 1984 ad. Watching him go from icon of capitalism to the world’s foremost philanthropist has been interesting, to say the least. As a relevant aside, I highly recommend the Netflix documentary on his life, it’s fascinating, and works well to provide context on where he’s coming from in writing this book.

The book itself does what it says on the tin: it ends with plans of action for preventing the sort of global climate disaster that we, as a species, have been gleefully sprinting towards ever since we realized those funky rocks we dug up would burn longer than the trees we were chopping down. And the plans aren’t just “buy an electric car and vote for green energy;” not only are there more action items than just that, there are plans for people depending on which hat they’re wearing. Sure, you the consumer can buy an electric car… but you the citizen can write your legislators, and you the employer can invest in R&D, and you the local government official can tweak building codes to allow for more efficient materials.

The first half, or more, of the book is an accounting of what’s driving climate change, and it’s a fascinating overview. Your first guess about the largest culprit, in broad categories, is probably wrong.

And in the middle, there’s a great deal of discussion of the technologies we’re going to need to get through this transition. As a life-long nerd, that was the part I enjoyed the most; as someone who’s very sold on the importance and utility of nuclear power, my absolute favorite moment was a throwaway reference to “we should be building nuclear-powered container ships.”1

Here at the end, where I usually say “I enjoyed this book, and I recommend it,” I’m still going to do that.2 But beyond enjoying the book, it feels like the single most important thing I’ve read… possibly ever. The pandemic is the definitive crisis of the last couple years; climate change is the definite crisis of this generation. Go read the book. Buy a copy, read it, and pass it along to someone else to read. Take notes, and follow the plans of action that’re applicable to you. Let’s go save the world.

  1. I may have set some kind of land-speed record going from “what the hell” to “that makes perfect sense.”
  2. It’s not that I like every book I read, it’s that, as a general rule, I don’t write reviews of the ones I don’t like. If you don’t have anything nice to say, don’t say anything at all.
Categories
Technology

Custom Queries in Vapor Fluent

While the QueryBuilder interface is pretty neat, it’s still missing some things. Recently, I needed a GROUP BY clause in something, and was rather unsurprised to find that Fluent doesn’t support it.1

Fortunately, it’s still possible to write custom SQL and read in the results. Make yourself a convenience struct to unpack the results:

struct MyQueryResult: Codable {
	let parentID: Parent.IDValue
	let sum: Double
}

(Strictly speaking, it can be Decodable instead of Codable, but as long as the Parent.IDValue (generated for free by making Parent conform to Model, I believe) is Codable, Swift generates the conformance for us.)

Now, in your controller, import SQLKit, and then get your database instance as an SQL database instance:

guard let sqlDatabase = req.db as? SQLDatabase else { 
	// throw or return something here
}

After that, write your request:

let requestString = "SELECT ParentID, SUM(Value) FROM child GROUP BY ParentID"

Note – your syntax may vary; I found that, using Postgres, you need to wrap column names in quotes, so I used a neat Swift feature to make that less painful:

let requestString = #"SELECT "ParentID", SUM("Value") FROM child GROUP BY "ParentID""#

If you want to use string interpolation, swap out \() for \#().

Finally, make the query:

return sqlDatabase.raw(SQLQueryString(requestString)).all(decoding: MyQueryResult.self)
  1. Entity Framework Core, which is an incredibly robust, full-featured ORM, only barely supports GROUP BY, so seeing this rather young ORM not support it isn’t all that shocking.
Categories
Development

Forms & Lists

If there’s one area where SwiftUI really shines, it’s forms and lists. I have one area of the app that’s meant as, at most, a fallback option for managing some data, and putting together that management interface took, oh, an hour? It was a breeze. Admittedly, it’s not the prettiest list I’ve ever made, but like I said: fallback option.

Screenshot of a list with a title, two items showing a date and value each, and an 'add' button.
Managing data points in a data set. Unpolished? Sure. Functional? Absolutely.
Screenshot of a Settings screen showing two lists, with an 'edit' button.

I was delighted to find that the automagic ‘edit’ button function handles multiple editable lists within the same View. As this is part of the ‘Settings’ screen, one of the three core screens in the app, it has received a bit more polish.

And I’ve continued to have fun building custom versions of the Picker control, with an expansion on my previous custom picker to support inline management of the above Data Sets and the addition of another one for picking a type of graph:

Screenshot of a picker showing three types of graphs.

At the moment, I’m showing a pretty basic dataset for these, but at some point I think I may create something a bit more visually interesting. The trick being, of course, that I can’t just random-number-generate the data, because I want all three to show the same data points, and since the user can also control the color, I want it to stay consistent if you leave, change the color, and come back.

(The solution here is probably to hard-code a data set, but where’s the fun in that?)

Categories
Development

File > New > Project…

I recently started working on a new development project. Or rather, a project I’ve been thinking about for a while, but just recently started developing – the first draft of the design is from almost a year ago, now, something that I worked on as a class project. But, mostly on a whim, I signed up for SwiftUI Jam, and took it as an excuse to start actually building the thing.

Now, normally my approach to projects is very Apple – refuse to admit I’m even working on something new until it’s complete, ready to present to the world. This time, though, the vague rules of the jam meant it had to be done at least somewhat in the open, and I figured I may as well do some proper write-ups as I go. Could be interesting.

I’m starting with… not the first thing I built, but the one that was the most fun so far. I’m doing the whole app in SwiftUI, and it really shines for building forms.

Screenshot of an iOS application showing a form: Title, Color, and Data Set; Comparison? is set to false.
Screenshot of an iOS application showing a form: Title, Color, and Data Set; Comparison? is set to true. The first three questions are repeated.
Screenshot of an iOS application showing a list to choose from, with groupings such as Activity featuring items like Calories Burned and Cycling Distance.

As I said, a fairly simple form – Title, Color, and Data Set, with the option to add a second of the same three items. The Data Set picker is a custom version of a Picker, because I wanted to give the options in a grouped list rather than just alphabetical order.

I suspect I’m going to be building a second custom Picker implementation sometime soon – this time, choosing from more visual options. Should be fun to put together.

Categories
Playlist

Playlist of the Month: March 2021

This has been the kind of month that managed to go by in the blink of an eye, and also lasted approximately a year and a half.

Cologne – Haux on Something to Remember – EP

Somewhere (feat. Octavian) – The Blaze on Somewhere (feat. Octavian) – Single

Slowly – ODIE on Slowly – Single

How It Was – Yoste on A Few Brief Moments – EP

Call Him – Noah Cunane on Call Him – Single

Come On – Will Young on Echoes

We’ll Be Alright – Yoste on A Few Brief Moments – EP

DON’T TELL THE BOYS – Petey on Checkin’ Up on Buds – EP

Spaces – Jaymes Young on Spaces – Single

FOR THE REST OF MY LIFE – Zack Villere on FOR THE REST OF MY LIFE1

Get My Fix – Adam Oh on Get My Fix – Single

Anticipation – Steve Benjamins on Anticipation – Single

Nuke the Moon – Epic Mountain on Nuke the Moon – Single

Falling Fire – Forester on A Range of Light

One Last, Last Time – Armen Paul on One Last, Last Time – Single

Crash Into Me – Petey on Crash Into Me – Single

The Light – Richard Walters on The Light – Single

Parachute (Piano Sessions) – Seafret on Parachute (Piano Sessions) – Single

SHE – Winona Oak on SHE – EP

Night Drives – Devan on Pink Noise – EP

Lo Vas A Olvidar – Billie Eilish & ROSALÍA on Lo Vas A Olvidar – Single

Cold Sets In – World’s First Cinema on Cold Sets In – Single

Drowning – Armen Paul on Drowning – Single

Time to Sink – Edwin Raphael on Time to Sink – Single

Arcade – Duncan Laurence on Arcade – Single

I’m Worried About You – Addict., REWiND & Achex on I’m Worried About You – Single

Red Run Cold – World’s First Cinema on Red Run Cold – Single

twentyfive – Yoste on twentyfive – Single

Boy on the Moon – GROUNDSTROEM on Boy on the Moon – EP

cheers – blackbear & Wiz Khalifa on cheers – Single

Loves You Like I Couldn’t Do – Duncan Laurence on Small Town Boy

Echo – Blakey on Echo – Single

You Don’t Look at Me the Same – Great Good Fine Ok & Yoke Lore on You Don’t Look at Me the Same – Single

Ricochet – Griffin Stoller on Ricochet – Single

emily – Jeremy Zucker & Chelsea Cutler on brent ii – EP

All Too Well – Jake Scott on All Too Well

At Night – Charles Fauna on At Night – Single

fan behavior – Isaac Dunbar on evil twin

(They Long To Be) Close To You – AJIMAL on (They Long To Be) Close To You – Single2

Lightning – Hayden Calnin on Lightning – Single

100 Miles – Steve Benjamins on 100 Miles – Single

Trust Issues – Spencer William on Trust Issues – Single

Wicked Game – Cal Trask on Wicked Game – Single

4Runner – Rostam on Changephobia

These Kids We Knew – Rostam on Changephobia

Keeping Your Head Up – JacobNeverhill & Nora Bart on Live at Home – EP

KING – LANKS & Yorke on SPIRITS PT.2

Mild Sanity (feat. Juletta) – Edwin Raphael on Staring at Ceilings – EP

Haircut – Petey & Miya Folick on Haircut – Single

Oh Dear, Oh Beaux – beaux on A Love Letter To the Moments Spent Outside3

skip.that.party – X Ambassadors & Jensen McRae on skip.that.party – Single

BOY WITH A BROKEN HEART – Noah Cunane, Lonr & Jutes on BOY WITH A BROKEN HEART – Single

Hate You + Love You (feat. AJ Mitchell) – Cheat Codes on Hate You + Love You (feat. AJ Mitchell) – Single

im a bad friend – Andy H on im a bad friend – Single

Night Like This – daydream Masi on Movie Scenes EP

Flustered Snowflakes – Pilar Victoria on Hi, My Name Is Pily! – EP

Hands On The Devil – Smeyeul. & Galvanic on Hands On The Devil – Single

Dead Girl! – Au/Ra on Dead Girl! – Single

The Worst – Spencer William on The Worst – Single

Feel It Coming – Woodlock on The Future of an End

(Title Redacted – Spoilers!) – Kristen Anderson-Lopez & Robert Lopez on WandaVision: Episode (Redacted) (Original Soundtrack)4

Nobody Loves Me (feat. ELIO) – Winona Oak on Nobody Loves Me (feat. ELIO) – Single

Te Olvidaste – C. Tangana & Omar Apollo on El Madrileño5

If I Didn’t Have You – BANNERS on If I Didn’t Have You – Single

Here We, Here We, Here We Go Forever – Mogwai on As the Love Continues6

Pompeii (Kat Krazy Remix) – Bastille on Remixed

Caves – Haux on All We’ve Known – EP

Calamity Song – The Decemberists on The King Is Dead

Whatever You Like – Anya Marina on Whatever You Like [Digital 45]

Hide & Seek (Imogen Heap Cover) – Amber Run**

I’m God – Clams Casino & Imogen Heap on I’m God – Single7

Lost In the World (feat. Bon Iver) – Kanye West on My Beautiful Dark Twisted Fantasy

I Can’t Lose You – Isak Danielson on Tomorrow Never Came

There for You – Daniel Allan on There for You – Single

RLNDT – Bad Bunny on X 100PRE

Rohypnol (feat. Fixway & Samer) – APRE on 19

Colours – Goth Babe on Smith Rock – Single

Video Games – Black Match on Basement Covers I – Single

Make Up Your Mind – Devan on Pink Noise – EP

Freedom – Jordan Hart on Only Pieces of the Truth

I’m Not Going Back – Kina & Mokita on I’m Not Going Back – Single

Blindside – Nathan Ball on Blindside – Single

Float On – Phil Good on Float On – Single8

Broken Heart Gang – Teflon Sega on Broken Heart Gang – Single

Losing My Religion – Shawn James on The Lake Wenatchee Sessions – EP9

  1. Between this and DON’T TELL THE BOYS I’ve apparently stumbled across subgenre of all-caps… whatever these two are grouped as. They go together, I just can’t think of a name for the group.
  2. Fun fact: every time this plays, I briefly get “come with me… and you’ll be… in a world of pure imagination” stuck in my head.
  3. Top pick this month!
  4. If you’ve seen it, you know what song this is. If you haven’t, don’t click! It’ll be much more fun that way.
  5. This is why RLNDT is back on the list, a bit further down – reminded me of it.
  6. This whole album is very good, but I’ve found that I have to be in the mood for Mogwai.
  7. This little Imogen Heap section brought to you by this video.
  8. This is a really excellent cover because it’s completely different from the original.
  9. Shawn James is just a whole mood sometimes, y’know?
Categories
Technology

Serving ‘files’ in Vapor

In my experience, dynamically generating a file, serving it immediately, and not persisting it on the server is a pretty common use case. In general, this is one of two things – either a PDF download, or a CSV. While my Vapor tinkering hasn’t yet given me an opportunity to generate PDFs on the server, I have had an occasion to create a CSV, and wrote up a little helper for doing so.

import Vapor

struct TextFileResponse {
    enum ResponseType {
        case inline, attachment(filename: String)
    }
    
    var body: String
    var type: ResponseType
    var contentType: String
}

extension TextFileResponse: ResponseEncodable {
    public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
        var headers = HTTPHeaders()
        headers.add(name: .contentType, value: contentType)
        switch type {
        case .inline:
            headers.add(name: .contentDisposition, value: "inline")
        case .attachment(let filename):
            headers.add(name: .contentDisposition, value: "attachment; filename=\"\(filename)\"")
        }
        return request.eventLoop.makeSucceededFuture(.init(status: .ok, headers: headers, body: .init(string: body)))
    }
}

That’ll work for any file you can assemble as text; CSV just struck me as being the most useful example. Use ResponseType.inline for a file you want displayed in a browser tab, and .attachment if it’s for downloading.

And if you’re doing a lot of CSVs, give yourself a nice little helper:

extension TextFileResponse {
    static func csv(body: String, name: String) -> TextFileResponse {
        .init(body: body, type: .attachment(filename: name), contentType: "text/csv")
    }
}
Categories
Technology

“All organizational systems fall on a spectrum from Calendar to To-Do List”

Something I said to a coworker recently. Largely inspired by listening to Cortex, and I felt like giving it a slightly more visual treatment.

Categories
Technology

Default Values in Vapor Fluent

My recent tinkering has been with Vapor, and while I mostly like their Fluent ORM, it has some rough edges and semi-undocumented behavior. At some point, I’ll feel confident enough in what I’ve learned through trial and error (combined with reading the source code – open source!) to actually make some contributions to the documentation, but for now, I’m going to throw some of the things I struggled with up here.

If you’re using a migration to add a column, and specifically want it to be non-null, you’ll need a default value. My first approach was to do a three-step migration, adding the column as nullable, then filling the default value on all rows, and then setting the column to be non-null, but that didn’t feel right. Eventually, though, I figured out how to express a DEFAULT constraint in Fluent:

let defaultValueConstraint = SQLColumnConstraintAlgorithm.default(/* your default value here */)

Then, in your actual schema builder call:

.field("column_name", /* your type */, .sql(defaultValueConstraint), .required)

Note that SQLColumnConstraintAlgorithm isn’t available from the Fluent module, you’ll need to import SQLKit first.

And here, a full worked example:

import Vapor
import Fluent
import SQLKit

struct DemoMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        let defaultValueConstraint = SQLColumnConstraintAlgorithm.default(false)
        return database.schema(DemoModel.schema)
            .field(DemoModel.FieldKeys.hasBeenTouched, .bool, .sql(defaultValueConstraint), .required)
            .update()
    }
    
    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema(DemoModel.schema)
            .deleteField(DemoModel.FieldKeys.hasBeenTouched)
            .update()
    }
}

(For context, I’m in the habit of having a static var schema: String { "demo_model" } and a struct FieldKeys { static var hasBeenTouched: FieldKey { "has_been_touched" } } within each of my Fluent models – it keeps everything nice and organized, and avoids having stringly-typed issues all over the place.)