Writing on Tablets Tech stuff from @yeltzland

Rewriting my WatchOS apps with SwiftUI

WatchOS apps can be so much nicer using SwiftUI, but it's still early days for the technology

I’ve been doing Paul Hudson’s excellent 100 Days of SwiftUI course, and decided to take the plunge and rewrite my WatchOS apps using SwiftUI.

Advantages of SwiftUI on WatchOS

I believe SwiftUI started life as a replacement for WatchKit and it really shows. It’s always been hard to build rich interfaces on WatchOS before, and Apple’s built-in apps couldn’t have been using WatchKit as they had many features simply unavailable to non-system developers.

With the power of SwiftUI, it’s now pretty easy to build rich animations, scrolling card views, and generally make first class apps that are much nicer to use.

The one (slight) downside is that you need to be running WatchOS 6.0 and above to run SwiftUI-based apps. However, I think Apple Watch users are pretty likely to be running the latest OS, and for my particular apps, the previous versions were pretty simplistic so didn’t see much usage anyway.

Changing to a SwiftUI mindset

SwiftUI is going to be a great way of developing apps for Apple’s multiple plaforms, although it’s still very new. The documentation leaves A LOT to be desired, and there are still bugs being fixed in each new version of Xcode/iOS.

The most interesting challenge was switching to a more reactive, event-driven way of propogating data changes into the app.

I won’t copy a whole bunch of code here, as both apps are open source and available here and here.

Updating the UI when the app became active

One particular challenge it took a while to figure out was how to trigger the animation of the progress control in Count The Days Left every time the app became visible.

There is an onAppear() function that can be added to a View. However, it turns out that is only run the first time a view appears, so when you reopen the app that is still in-memory, it won’t get called again.

This also meant that if the app stayed in memory overnight, it wouldn’t update the view the next day - which would obviously then be out of date 😟

The solution

  • In the data model class the the view observes, add a PassthroughSubject object:
import SwiftUI
import Combine

class WatchDaysLeftData: ObservableObject {
    
    @Published var currentPercentageLeft: String = ""
    @Published var currentTitle: String = ""
    @Published var currentSubTitle: String = ""
    @Published var percentageDone: Double = 0.0
    
    let modelChanged = PassthroughSubject<(), Never>()
    ...
  • The observable object also exposes a updateViewData() function, that (amongst other things):
    • Resets the self.percentageDone to be 0.0, and then calls self.modelChanged.send(())
    • Calculates the correct self.percentageDone, and then calls self.modelChanged.send(()) again
  • The View class receives messages from the modelChanged object, and updates the view using an animation:
struct ProgressControl: View {
    @State private var animationProgress: Double = 0.0
    @ObservedObject var model: WatchDaysLeftData

    // Code cut for clarity

    var body: some View {
      ZStack {
        // Code cut for clarity

        Arc(progress: self.animationProgress)
        // Code cut for clarity
          .onReceive(model.modelChanged, perform: { _ in
            withAnimation(.easeInOut(duration: self.duration)) {
              self.animationProgress = self.model.percentageDone
            }
          })
        }
    }
}
  • Obviously this “animates to 0.0” and then “animates back to the real value” because two separate messages are sent to the View.

  • Finally, the actual data model passed through into the View is a property on the extension delegate, so we can call self.dataModel.updateViewData() in the extension’s applicationDidBecomeActive() event handler, which triggers a “re-animation” each time the app becomes active.

Did that make sense?

As you can see from the video below, this isn’t ideal, and to be honest feels a bit hacky. It would have been MUCH nicer if the View’s onAppear() worked as the name implies, and runs every time the view actually appears!

It could also be that I’m still learning SwiftUI - so definitely let me know if you have a better solution!

Videos showing what I’ve been trying to say

Here’s the new Count The Days Left, showing the nice animation (with room for improvement!):

… and here’s how my new, improved Yeltzland app looks. The card view is perfect for Watch apps, and was as easy adding .listStyle(CarouselListStyle()) to the List View showing the fixtures and results data.

Please excuse the scroll glitch, as my home-made video setup meant I struggled to turn the digital crown with my wrist at a funny angle 🙄

Summary

I’m really happy how much better my two WatchOS apps are now, and SwiftUI is truly game-changing (not just for the Watch!)

Once I decide to bump the main Count The Days Left app up to support only iOS13 and above, I should be able to reuse most of the SwiftUI code pretty much as is, which will be fantastic.

I’m also considering building Mac Catalyst versions of the apps at some point, so again could consider doing those in SwiftUI. However other issues (around some 3rd party dependencies) don’t make this easy, so that may be a while off yet.

Count The Days Left iOS Swift SwiftUI

Fixed an issue using WkWebView when session cookies were intermittently dropped

This bug took about 2 years to fix!

Fingers crossed, but I think I’ve finally fixed an irritating issue in my Yeltzland iOS app, where session cookies were being intermittently dropped in WkWebView.

This was especially painful when using the Yeltz Forum as you’d be logged out of your account every so often 😞.

The problem is (always) threading

I’ve spent ages over the years trying different solutions to see what’s happening, and as far as I can the root cause is that WkWebView actually runs on a different process than the app, which can prevent cookies being synched properly - and hence being dropped if you are unlucky.

The solution

I changed the code to use a single WKProcessPool for all web views in the app, and then instantiate each WkWebView is created using the following code:

lazy var webView: WKWebView = {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    
    // Use a single process pool for all web views
    let webConfiguration = WKWebViewConfiguration()
    webConfiguration.processPool = appDelegate.processPool

    let webView = WKWebView(frame: .zero, 
                    configuration: webConfiguration)
    webView.navigationDelegate = self
    
    return webView
}()

This was based on a very helpful comment by LinhT_24 in the Apple Developer Forums, for which I’ll be eternally grateful.

I’ve been running this code for nearly two weeks now without being logged out from the forum, so I really hope this is fixed.

Hopefully this post might help someone else with the same problems too!

iOS

Adventures in Siri Shortcuts automation

Trying out the new Shortcuts in iOS 13 - with promising but mixed results

I’ve been looking forward to the new automation options in Siri Shortcuts in iOS 13 for a while.

Previously I’ve used a hodge-podge of IFTTT, Launch Center Pro and Shortcuts to do some of what I’ll describe below, but new trigger options opens up a whole new world of possibilities.

Everything is still a bit buggy right now (as of iOS 13.1.3), but shows a lot of promise.

Automatically enabling VPN away from home

I’ve just started using Cloudflare’s free 1.1.1.1 VPN app, which so far seems reliable, fast and a lot safer when using coffee-shop WiFi networks.

I don’t want to enable the VPN when at home (there are occasional geo-location issues when trying to stream videos), but almost always want to when not on my home network.

The 1.1.1.1 app offers actions to enable or disable the VPN, so it was trivial to write a shortcut that:

  1. Is triggered on connection to any WiFi network
  2. Checks the current WiFi network name
  3. If it is my home network, disables the 1.1.1.1 VPN
  4. If it’s any other network, enables the 1.1.1.1 VPN

This is great, as I was continually forgetting to enable/disable the VPN before - now I get a notification every time I switch networks, and tapping on it quickly switches the VPN to the state I almost certainly want.

Playing Audio

Starting up Spotify

I’ve written previously about my shortcut where I started Spotify using NFC and Launch Center Pro, so it was easy to migrate this to using the NFC triggers in Shortcuts instead.

One issue was that the NFC tags I’d bought from LCP were encoded to a LCP launching URL, so I had to purchase some new tags.

The next enhancement was to setup a trigger to run the same shortcut when my phone connects to the Bluetooth speaker in my home office. This is so much easier to use than even the NFC tag - I simply turn on the speaker, and then tap the notification to fire up Spotify and get it running.

Podcasts in Overcast

The other audio I listen to regularly are podcasts in Overcast.

Overcast does have some automation hooks, although they seem a little flaky in iOS 13. I’ve written a shortcut that asks me if I want to play either Overcast or the Spotify shortcut mentioned above from a list.

I have 2 triggers to run this combined shortcut:

  • When I connect my Bluetooth headphones
  • An NFC tag in my car

Showing my Shopping List

I’ve previously built a completely over-engineered shopping system based around Todoist, IFTTT location triggers, IFTTT voice integration and an AWS Lambda function!

This worked pretty well, and basically location triggers in the IFTTT iOS app called a script which read my Todoist Shopping List when I’m near the supermarkets in Hexham, and sent an alert if there was anything I needed.

I’ve moved the location trigger in Shortcuts, but also I’m experimenting in going all-in with the built-in Reminders app, and not using Todoist any more.

This means all of the shopping list logic can be done on-device (which makes it a bit more reliable), and it’s also easy to share the list with my family.

The integration with my multitude of Amazon Echos and Google Assistants also needed a couple of custom IFTTT applets, but they were very simple to write.

It’s all a bit less Heath Robinson now, but works just as well.

Enhancements/Missing Features

I’m pretty happy with where my automation sare so far, but there’s definitely room for improvement.

First of all, the triggers - in particular the location ones - seem very flaky. Shortcuts itself seems a bit hit and miss at times, and my guess is that if any script has had problems, the app somehow hangs/is blocked, which stops the location triggers firing.

Next, it would be great if more types of triggers could run automatically rather than needing you to tap on a notification. I know Apple are very strong on privacy, but there should be a way to let me accept the “risks” and let more shortcuts be able to be triggered without needing my intervention.

Finally, there is no integration yet with the Apple Watch. I believe some Watch integration may be coming in iOS 13.2, but it would be great to be able to both start shortcuts from the watch as well run them automatically based on triggers. For example, I’d like to be prompted to start a workout as soon as I leave my house, rather than wait for the automatic “after 10 minutes walking” feature to sometimes kick in.

Summary

Just implementing these three automations has been great, and shows of the power of Shortcuts to extend the functionality of my expensive devices in truly useful ways.

If the automation system was a bit less buggy, and a bit less restrictive on needing so much user intervention, it would be truly fantastic.

Hopefully this will be coming soon! 🤞

Shortcuts

Privacy Policy Update

An update to an earlier post - removing Crashlytics and Fabric from my apps

Last year I wrote about my Privacy Policy, outlining the approach I take on my mobile apps, and trying to justify the trade-offs I’m making.

This is a follow-up on what’s changed in the mean time.

Removing Crashlytics and Fabric

I’ve been trying to remove 3rd party code from my apps as much as possible.

This is a really more of a practical concern - fewer dependencies makes it easier to change the code/keep up to date on OS changes/be agile - than privacy worries (not really knowing what 3rd party libraries are actually doing).

Therefore I decided to remove Crashlytics and Fabric from my own apps.

Both Apple and Google offer built-in basic analytics and crash reporting via the App Store Connect and Play Store servers respectively. I realised for my own apps, this is perfectly sufficient for what I need.

All I really care about is finding out about crashes or issues, plus every so often knowing the breakdown of OS versions my users have so as to plan when I can drop support for older devices.

This isn’t an anti-Google stance, as I’m still using Firebase and Google Mobile Ads. It’s just why give my user’s data away any more than I need to?

Summary

Your mileage may vary, and I’m not sure I’d advise this approach for all my clients, but I’m happy this will work well for me and my users.

Privacy

A Shortcut for sharing Live Photos

Replacing some average iOS apps for converting Live Photos with a simple Shortcut

Here's one I made earlier

I’ve been searching for an easy way to share iOS Live Photos on social media for a while, and have tried a whole bunch of frankly pretty average apps without being very happy with any of them.

Last week Jason Kottke shared on Twitter a shortcut that could convert a Live Photo into a video, which looked close to what I wanted. I’d never considered I could write my own converter before!

Below is a screenshot of my version. This one will convert a Live Photo into a looping animated GIF. A little trick I also learnt was if you use the “Quick Look” block at the end, you get a preview of how the GIF looks, as well as a share button from where you can share or save it as appropriate.

The image of Seljalandsfoss from last year’s trip to Iceland above was made using the script - and I’m very happy with the results!

Live Photos shortcut

Shortcuts Live Photos