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

Categories
Review

“The Last Thing He Wanted”

Joan Didion

This book took a while to really capture my attention, in terms of time. In terms of how far into the book it took, I suspect it was about the usual amount of time it takes a book to grab me. The distinction being, usually I read books like I’ve got a grudge, like I’m trying to see how fast I can cram all these words into my brain. Not so, with this one — I’d read a chapter or two, and put it down. Sometimes for a couple minutes, so I could sit and process a bit, and then pick it up and continue; other times, it’d be a day or two before I tried again.

All in all, this isn’t the kind of book I tend to go for. It feels much more Literary than my default — which is largely the writing style, but something about the paper and the typesetting makes it feel like the kind of book I’d read for English class in high school, filling it with notes and highlights and a ridiculous amount of sticky notes.

By the end, it feels… semi-coherent. Which, by then, you’ve grown used to, because at the beginning it’s entirely incoherent. The writing style is “first draft of a book by somebody who got a doctorate on a specific week of history and has no grasp of the concept of expert blind spot.”

At the end, though, I liked the book. Apparently it’s been developed into a Netflix film, the cover tells me; I may watch it, because I can’t imagine the film adaptation at all feeling like the book.

In writing this, I can also tell just how much Didion’s writing style has influenced mine, at least at the moment. Consider this a cheap knock-off of a demo. And then go read the real thing, instead.

Categories
Review

“The Counterfeit Viscount”

Ginn Hale

I don’t think I read the word ‘viscount’ a single time in this book without thinking of Enola Holmes, but that was a fun movie, and this was a fun book, so it all worked out okay.

Like the last book of Hale’s that I read, there’s a great deal of fun worldbuilding going on in a short read. Another alternate history thing, in an entirely different direction, and once again it provides a fun backdrop for a simple enough story.

Admittedly, the mystery itself is a bit convoluted, but it feels like a backdrop for the romance angle, so it can get away with it.

It’s a short read, so I think this short review will suffice. It’s a fun little story, with a silly little romantic plot, and sometimes that’s what you need. If that’s what you’re in the mood for, give it a read.

Categories
Review

“Floodtide”

Heather Rose Jones

The introduction to this book was familiar enough that I did a quick search and found out I’ve read another book in this universe.1 I may go back and reread that one now, in fact, because I think that “Floodtide” did a better job of introducing the system of magic in a way that makes sense to my brain.

It’s also, largely, a much more human-scale story. The protagonist isn’t changing the world, she’s just trying to get through life, finding a little bit of happiness along the way. Sure, she has friends changing the world, living a grand, romantic life, and she’s determined to help them do that as best she can, but she’s still… a regular person. Sometimes, it’s nice to read things like that — it’s what got me watching Agents of SHIELD back when it first aired, after all.2

It reminds me, a little, of the idea of a space opera. There’s all sorts of large-scale things happening in the backdrop, but the actual core of the story is about the characters and how they’re doing, why they make the choices they do, that sort of thing.

I’m not certain how well I’m selling this book, but I did quite like it. Give it a go.

  1. That was more than three years ago, now? Somehow, in my head, none of my ongoing projects have actually been ongoing that long, and yet, here I am, several years into writing little book reviews.
  2. And, y’know, once Agents started being about saving the whole world instead of just, y’know, regular people trying to exist in a world with superheroes, I gave up on it.
Categories
Review

“The Strange Case of the Alchemist’s Daughter”

Theodora Goss

This book reminded me of the 2017 remake of The Mummy. Which, I must admit, sounds like an insult, but hear me out: this book is what that movie wanted to be.

The premise is fairly simple: what happened to Dr. Jekyll’s family? (And, further, what happened to any of the background characters in any of the popular novels of the time?)

And from that question, Goss made a marvelously interesting story. She’s establishing a shared universe for a lot of these stories, pulling together the literary zeitgeist of the whole period into a single interlinked whole, in a delightful way.

Beyond that, the actual writing style is very well done. There’s a main protagonist, and the story is mostly told from her viewpoint, but there are interjections from the other characters, and you learn fairly quickly on that, though she’s the protagonist, she’s not actually the one wring — just giving the occasional editorial comment. It reads like the, oh, third draft of a book, where you can still see all the margin notes thrown in by the various people reading through and remarking on their own perspective of the events in the book.

Very early on, this disorganized style is used for what I think is the most interesting piece of foreshadowing I’ve read in quite a while — one of the more impatient characters leads in with “no, no, you should start in medias res, like this” and suddenly we’ve skipped forward several chapters, to a very exciting scene, for something like half a paragraph, before we’re pulled back to where we were with “now hold on, they won’t know what’s going on if we jump right to there!” It is, frankly, delightful.

I very much enjoyed this book — as evidenced by my reading it in a single sitting — and highly recommend it. Give it a go, and, if you need me, I’ll be adding the sequels to my wish list.

Categories
Review

“The Voyages of Cinrak the Dapper”

AJ Fitzwater

I very nearly gave up on this book halfway through, the point of putting it down and not picking it back up again until a couple months had passed. I’m glad I gave it that second chance, though, because once I was over that hump, I quite enjoyed it.

That midpoint was where the amount of ‘fantasy’ in this fantasy novel jumped up by a lot. Because, yes, it’s a book about a capybara pirate, so of course the whole thing is a fantasy novel.1 But where it nearly lost me was in changing from “here’s a bunch of tropes that I’m using to make some characters I like” to setting up a whole new mythology unlike any I’ve seen before. And if I’d given up, that would’ve been a shame, because this new mythos is downright beautiful. I can’t honestly say that I follow every part of what’s going on, but I also can’t really say that I mind, because, again: beautiful.

I’m trying very hard not to spoil anything, because it all ties together so well. Suffice it to say that if you aren’t invested by the end of the story where Agnes makes her first appearance, you have my permission to give up on the remainder of the book.

Hopefully that won’t happen, though. Give it a try.

  1. You could also make it science fiction, assuming that there’s been an uplift and possibly some sort of apocalypse in the interim, but that’s pushing so close to the “sufficiently advanced technology” line that it may as well be a fantasy novel at that point.
Categories
Review

“What If?”

Randall Munroe

A magnitude 15 earthquake would involve the release of almost 1032 joules of energy, which is roughly the gravitational binding energy of the Earth. To put it another way, the Death Star caused a magnitude 15 earthquake on Alderaan.

This is a fun book to recommend, because unlike most books, there’s a demo available online. Go read that, and if you like it, the book contains more. It also has a very literal subtitle: “serious scientific answers to absurd hypothetical questions.”

Munroe has had a fascinating career to date, and I remain an avid fan of his webcomic. It was definitely a formative influence on the nerdier side of my sense of humor,1 and continues to make me laugh an average of slightly more than three times a week.2

This is a fairly good book for reading in small chunks – each ‘chapter’ is only a few pages long, and there’s no need to read them in any specific order.

All in all, it’s a fun read, and I definitely recommend it.

  1. And, in writing that, I’m having fun imagining his reaction to reading that.
  2. Three new comics a week, and the average is above that because sometimes I wind up hitting the ‘random’ button a few times and laughing again.