Radio MilwaukeeĀ AppleScript

I’ve been listening to Radio Milwaukee a lot lately. They play it at a lot of my favorite coffee shops in Milwaukee that I haven’t been able to visit for a while.

I like to listen to music in the Music app (previously iTunes) because I have AirPlay speakers in my office and around the house. This script opens the Radio Milwaukee stream and hides the Music app.

tell application "Music"
	open location "https://wyms.streamguys1.com/live?platform=88nine"
	play
	set visible of every window to false
end tell

The Day I Met JoeĀ Biden

The Friday before the 2016 Presidential Election, I was at the same coffee shop that I had been going to almost every day for years. At one point I looked up from my laptop and noticed someone with an ear piece standing outside the window. After working for a while longer, I remembered that Vice President Biden was speaking in Madison that morning, campaigning for Hillary Clinton and Russ Feingold, who was running for his old Senate seat again.

A little while later, I noticed that Secret Service had actually stopped letting people in without Press credentials and there were many reporters inside and outside the cafe.

The next thing I remember is the motorcade coming around the corner and stopping right in front of Colectivo. I just found the moment in Slack where I was in the middle of a conversation when I said:

brb, joe biden is here
i think he’s literally walking into Colectivo
he is

First Russ Feingold came in. I had been sitting in the front of the cafe when all this started, so I was one of the first people to shake his hand when he walked in the door. (It probably didn’t hurt that I had a huge Russ Feindgold bumper sticker on the cover of my laptop šŸ™ƒ)

Then Joe Biden came in and shook everyone’s hand. He noticed someone with a Green Bay Packers phone cover and told a story about how he got out of school early on Mondays if the Packers won. Then he walked into the middle of the restaurant and announced, “I’m Joe Biden and I work for Russ Feingold.”

I think about this day a lot when I encounter people that are less than enthusiastic about the idea of a President Biden. He wasn’t my first choice in the Democratic primary, but he is qualified to be President, he’s a good person, and he would make things better for most people instead of worse. In a lot of ways, he’s the opposite of our current President.

iOS 14 SmartĀ Stack

For me, the best new iOS 14 feature is the Smart Stack widget. Widgets in general are neat, but I’m hoping the Smart Stack technology will be implemented in other parts of iOS as well.

If you’re not aware, Smart Stack is basically a way for apps to report when they have something interesting to report. In the example below, it had just started raining in Madison, so the weather widget was surfaced.

The obvious extension of this is for Watch complications. Having a temperature complication that switched to a weather conditions complication to warn you about pending inclement weather is a simple example that would make the Watch more useful and easier to use.

Switching to Calendar when you have a meeting coming up, Reminders when you have a task due, or Stocks when the stock market would be very convenient.

A Smart Stack on the lock screen would also be interesting. While you can already put widgets in the dashboard to the left of the lock screen, a single, customizable Smart Stack on the lock screen itself would be able to quickly surface similarly important information about weather, an important meeting, breaking news, and so on.

Fingers crossed for iOS 15. šŸ¤ž

IPv6 PACĀ Support

If you’re not familiar with a Proxy Auto-Configuration (PAC) file, it’s a JavaScript function that determines whether web requests should be forwarded to a proxy server or not.

There’s a minimal set of JavaScript functions defined that you can use to conditionally send your web traffic through a proxy. One of those functions, isInNet(), returns true if an address (an IPv4 address) is included in a given network range. Unfortunately there are no functions in this standard set that provide similar support for IPv6 addresses.

There are many IPv6 parsing libraries on GitHub, but all of them depend on at least a few npm packages. I’m normally not one to complain about npm dependencies, but this is once instance where the dependency model is not really tenable.

Here I’ve put together a copy/pastable inIPv6Range() function that provides limited functionality for determining whether a given IPv6 address is in a predetermined range.

function expandIPv6( ipv6 ) {
	const parts = ipv6
		.replace( '::', ':' )
		.split( ':' )
		.filter( p => !! p );
	const zerofill = 8 - parts.length;

	// Fill in :: with missing zeros
	return ipv6
		.replace( '::', `:${ '0:'.repeat( zerofill ) }` )
		.replace( /:$/, '' );
}

function parseIPv6( ipv6 ) {
	ipv6 = expandIPv6( ipv6 );

	// Check is valid IPv6 address
	ipv6 = ipv6.split( ':' );
	if ( ipv6.length !== 8 ) {
		return false;
	}

	const parts = [];
	ipv6.forEach( function( part ) {
		let bin = parseInt( part, 16 ).toString( 2 );
		while ( bin.length < 16 ) {
			// left pad
			bin = '0' + bin;
		}

		parts.push( bin );
	});

	const bin = parts.join( '' );
	return parseInt( bin, 2 );
}

function inIPv6Range( ipv6, low = '::', high = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' ) {
	ipv6 = parseIPv6( ipv6 );
	low = parseIPv6( low );
	high = parseIPv6( high );

	if ( false === ipv6 || false === low || false === high ) {
		return false;
	}

	return ipv6 >= low && ipv6 <= high;
}

If you clean this up or add CIDR support, let me know!

SwiftUI Webview with a ProgressĀ Bar

Update: (Nov 16, 2021) – This now includes better constraint management from feedback on GitHub.

If you’re not familiar, SwiftUI is the new UI framework for building user interfaces in Swift apps. If you squint, the declarative syntax is vaguely reminiscent of contemporary JavaScript frameworks like React.

With a declarative Swift syntax thatā€™s easy to read and natural to write, SwiftUI works seamlessly with new Xcode design tools to keep your code and design perfectly in sync. Automatic support for Dynamic Type, Dark Mode, localization, and accessibility means your first line of SwiftUI code is already the most powerful UI code youā€™ve ever written.

developer.apple.com/xcode/swiftui/

The API is still fairly immature. When you get beyond the sample apps, it’s hard to build anything real with the components that come out of the box. Components that you would expect to see if you’re familiar with UIKit, like WKWebView and UIActivityIndicatorView don’t exist in SwiftUI yet.

Luckily, it’s not that hard to create them yourself.

To get started with a basic view, you need an object that implements UIViewRepresentable. A simple Webview could look like this:

struct Webview: UIViewRepresentable {
	let url: URL

	func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView {
		let webview = WKWebView()

		let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
		webview.load(request)

		return webview
	}

	func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<Webview>) {
		let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
		webview.load(request)
	}
}

Progress Bar Example

It’s also possible to model a UIViewController by implementing UIViewControllerRepresentable.

For example, a view controller that renders a web view with a progress bar:

class WebviewController: UIViewController, WKNavigationDelegate {
	lazy var webview: WKWebView = WKWebView()
	lazy var progressbar: UIProgressView = UIProgressView()

	deinit {
		self.webview.removeObserver(self, forKeyPath: "estimatedProgress")
		self.webview.scrollView.removeObserver(self, forKeyPath: "contentOffset")
	}

	override func viewDidLoad() {
		super.viewDidLoad()

		self.webview.navigationDelegate = self
		self.view.addSubview(self.webview)

		self.webview.frame = self.view.frame
		self.webview.translatesAutoresizingMaskIntoConstraints = false
		self.view.addConstraints([
			self.webview.topAnchor.constraint(equalTo: self.view.topAnchor),
			self.webview.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
			self.webview.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
			self.webview.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
		])

		self.webview.addSubview(self.progressbar)
		self.setProgressBarPosition()

		webview.scrollView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)

		self.progressbar.progress = 0.1
		webview.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
	}

	func setProgressBarPosition() {
		self.progressbar.translatesAutoresizingMaskIntoConstraints = false
		self.webview.removeConstraints(self.webview.constraints)
		self.webview.addConstraints([
			self.progressbar.topAnchor.constraint(equalTo: self.webview.topAnchor, constant: self.webview.scrollView.contentOffset.y * -1),
			self.progressbar.leadingAnchor.constraint(equalTo: self.webview.leadingAnchor),
			self.progressbar.trailingAnchor.constraint(equalTo: self.webview.trailingAnchor),
		])
	}

	// MARK: - Web view progress
	override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
		switch keyPath {
		case "estimatedProgress":
			if self.webview.estimatedProgress >= 1.0 {
				UIView.animate(withDuration: 0.3, animations: { () in
					self.progressbar.alpha = 0.0
				}, completion: { finished in
					self.progressbar.setProgress(0.0, animated: false)
				})
			} else {
				self.progressbar.isHidden = false
				self.progressbar.alpha = 1.0
				progressbar.setProgress(Float(self.webview.estimatedProgress), animated: true)
			}

		case "contentOffset":
			self.setProgressBarPosition()

		default:
			super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
		}
	}
}

Then you can implement that as a UIViewControllerRepresentable like so:

struct Webview: UIViewControllerRepresentable {
	let url: URL

	func makeUIViewController(context: Context) -> WebviewController {
		let webviewController = WebviewController()

		let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
		webviewController.webview.load(request)

		return webviewController
	}

	func updateUIViewController(_ webviewController: WebviewController, context: Context) {
		let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
		webviewController.webview.load(request)
	}
}

You can see a working example of this view controller over on GitHub.