Wasted Time Pro – Launch Date

Well, I’ve finally done it. I have submitted an app that will launch on the same day as a new piece of Apple hardware. I have taken my simple application – Wasted Time, and did a total re-write over the last year to better take advantage of SwiftUI and the latest Swift APIs.

Wasted Time version 1.0 (2010)

I changed the underlying architecture to use the new Swift @main entry point, and even got rid of the old AppDelegate model I used to use. AppDelegate was the model that you used way back in Objective-C programs.

The basic functionality of Wasted Time hasn’t changed since 2010, however I did remove Twitter a few years back.

Here’s what it will look like on the Apple Vision Pro.

I can’t wait to get ahold of the Apple Vision Pro hardware, so I can actually see my code running outside of the simulator.

SwiftData / SwiftUI Combo Frustrations

The last few posts have been about my re-write of my personal application to address greeting cards.

This has been a very frustrating effort as I’ve tried to restructure the app’s UI to better adhere to Apple’s design principles in order to allow the user edit both a recipient’s Address and to add their own event types. This has required me to do the following things:

  • Restructure the database itself, going from only two tables:
    • A recipient of a card
    • An instance of a card that a recipient has been sent
  • To four tables:
    • An Event Type, i.e. Christmas, Birthday, etc.
    • A recipient, name and address
    • A gallery of greeting cards
      • With pictures, event types, descriptive name, and a URL where you can find it
    • An instance of a specific card for that a recipient has been sent

This approach would allow me to dramatically reduce the size of the database by only having one image for each Greeting Card. And it would also allow me to dramatically increase load speed, since loading the image would only occur as an AsyncImage (and could be cached, since the same image would be used by multiple instances).

Apple is pushing people to start using NavigationStack, with a navigationPath so that you can push to any screen and pop back thru the stack. However, I would like to structure the app with a NavigationSplitView since it is designed for iPad and iPhone. The basic idea would be to allow the user to start with picking do they want to add a recipient, an eventType, or a card to the Card Gallery. Those would each be a separate NavigationStack so you would get a list of all the existing items (events, recipients, or greeting cards) in column two, and then a detailed listing of all the specific cards that match it in the third column. Simple right?

The third column would be the “Detailed” view, so for a recipient, you’d get a LazyVGrid with all the Cards that they have received, sorted by date. For an EventType, you’d get a LazyVGrid with all the Cards for all recipients with that specific event sorted by date. And finally for the Card Gallery, you’d get all the cards by type.

While there is still a lot of UX improvements I can do here, the basic structure here is done using the original approach.

As I try and figure out the three column view with the NavigationSplitView, I am wondering if I can still use the NavigationPath. Also, since my SplitView starts with a list of the enumeration states (and I’ve tried both “Buttons” and just “Lists”, I can’t seem to highlight the selected item, so now I have to a title in the center column. It’s a little bit better. But for some reason you see that the “Cards Sent” in the by recipient view, is showing up in Black. It blinks for a second in the right color.

The other feature I have added was a custom picker for the image’s in the gallery by event type.

As you can see the images show up at the bottom of the window for some reason. it is very frustrating, as I am trying to use a SwiftUI Form to give the nice gray background and grouping of various sections of information.

VStack {
   Form {
     Section("Card Information") {
        Picker("Select type", selection: $selectedEvent) {
            Text("Unknown Event").tag(Optional<EventType>.none)
            if events.isEmpty == false {
                 Divider()
                 ForEach(events) { event in
                    Text(event.eventName)
                       .tag(Optional(event))
                 }
             }
         }           
         DatePicker(
            "Event Date",
            selection: $cardDate,
            displayedComponents: [.date])
         }
      }
      .padding(.bottom, 5)
      if selectedEvent != nil {
         GreetingCardsPicker(eventType: selectedEvent ?? EventType(eventName: "Unknown"), selectedGreetingCard: $selectedGreetgingCard)
      }
   }
}

If I place the GreetingCardsPicker in a Section it collapses to a single line (completely unusable). So for now, I am stuck with the cards all showing up at the bottom. Hopefully I can figure this one. At the same time, right now, the “GreetingCardsPicker” is not returning the selected item. As a matter of fact the .onTapGesture code doesn’t even execute.

Hope to make more progress soon.

Card Tracker Rewrite Update

This weekend I had a major milestone of the rewrite. All features are working (except edits). I was successful in adding a new feature of allowing for looking at all the cards sent for a specific event, i.e. Show me all the Christmas cards by date and recipient.

This new feature is something I’ve been wanting to add for a long time. What’s great about it is that all the hard work I did recently to allow for a print of all the cards for a single recipient, now also works for ll the cards for a specific event.

After this update, I finally started moving five years of card history over to the new version of the app. This reinforced a big issue I have been ignoring. Space bloat! The app currently takes every card’s picture and attaches it to a specific recipient’s event. Meaning that if two people are getting the same Thank You card, then it takes up twice as much space. As you an image, Christmas cards would take up a ton of space.

To that end, I’ve decided to implement a Card Gallery feature. This will all a user to take a picture (or scan) of a specific card, and then use it for all the people who get the same card. This will dramatically improve performance (thanks to caching by SwiftData), and will also radically reduce storage requirements.

Of course adding this feature means I am going to have to adjust the UI and delay my desired release of the app until I get that (and the edit feature) working. Hopefully the next few weeks will allow me to make major progress.

Card Tracker ReWrite

For the last five years or so, I’ve been working on a pet project to track the various greeting cards we send out each year. The initial version had a lot of UI problems, so I used it as an opportunity to learn SwiftUI.

The second major rewrite was to add CloudKit integration so that I could sync my cards across devices. That showcased significant issues with my database design. So I’ve been trying to work thru rewriting it lately with SwiftData.

Before I could do that, I had to enable a way to print out all the information in the application. After five years of usage we had hundreds of cards we’ve sent to people and I didn’t want to lose this data, nor was I sure I’d be able to successfully migrate from CoreData and CloudKit to SwiftData. I also knew that my database redesign was going to be very aggressive, and it was time to clean up the storage requirements.

Today I’ve been able to finish in less than a day, a significant portion of the problematic aspects of the application. I know now create new card types, list the card types, list all the recipients of the cards, and filter each of these lists.

It has been loads of fun working on this over the course of a weekend, and I am hoping during my upcoming year end vacation to not only completely rewrite the applications, but to also reenter five years of cards. This effort will allow me to validate that the new design is not only faster, but easier to use!

Can’t wait to share more!

Deletes in Holiday Card Tracker

I’ve been working on a Greeting Card tracking app, in my spare time for years now, five to be exact. I may get it on the App Store one day, but primarily it is just used to track the cards my wife and I send to friends and family. I’ve re-written it a few times, going from UIKit to SwiftUI, and then improving the Swift code to do CoreData with CloudKit syncing. I am hoping to rewrite it sometime in the next year to fully SwiftData, but right now I am struggling with deleting cards correctly.

Conceptually, deleting a child object in a parent-child relationship in CoreData is not hard. You just have to delete the child and CoreData should handle the rest; however, I am showing all the children in a LazyVGrid, so that you can get a quick and easy overview of all the cards you have sent to a single recipient.

As you can see above, we have at least 6 cards visible at once, and each card will allow you to edit, view in detail, or delete. Each of these “events” is a separate SwiftUI View, and on top of it is the MenuOverlayView:

//
//  MenuOverlayView.swift
//  Card Tracker
//
//  Created by Michael Rowe on 4/16/22.
//  Copyright © 2022 Michael Rowe. All rights reserved.
//

import SwiftUI

struct MenuOverlayView: View {
    @Environment(\.managedObjectContext) var moc
    @Environment(\.presentationMode) var presentationMode

    @State var areYouSure: Bool = false
    @State var isEditActive: Bool = false
    @State var isCardActive: Bool = false

    private let blankCardFront = UIImage(contentsOfFile: "frontImage")
    private var iPhone = false
    private var event: Event
    private var recipient: Recipient

    init(recipient: Recipient, event: Event) {
        if UIDevice.current.userInterfaceIdiom == .phone {
            iPhone = true
        }
        self.recipient = recipient
        self.event = event
    }

    var body: some View {
        HStack {
            Spacer()
            NavigationLink {
                EditAnEvent(event: event, recipient: recipient)
            } label: {
                Image(systemName: "square.and.pencil")
                    .foregroundColor(.green)
                    .font(iPhone ? .caption : .title3)
            }
            NavigationLink {
                CardView(
                    cardImage: (event.cardFrontImage ?? blankCardFront)!,
                    event: event.event ?? "Unknown Event",
                    eventDate: event.eventDate! as Date)
            } label: {
                Image(systemName: "doc.text.image")
                    .foregroundColor(.green)
                    .font(iPhone ? .caption : .title3)
            }
            Button(action: {
                areYouSure.toggle()
            }, label: {
                Image(systemName: "trash")
                    .foregroundColor(.red)
                    .font(iPhone ? .caption : .title3)
            })
            .confirmationDialog("Are you Sure", isPresented: $areYouSure, titleVisibility: .visible) {
                Button("Yes", role: .destructive) {
                    withAnimation {
                        deleteEvent(event: event)
                    }
                }
                Button("No") {
                    withAnimation {
                    }
                } .keyboardShortcut(.defaultAction)
            }
        }
    }

    private func deleteEvent(event: Event) {
        let taskContext = moc
        taskContext.perform {
            taskContext.delete(event)
            do {
                try taskContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

It’s a pretty simple view, with a menu overlay to Edit, View, or delete. The delete function just calls CoreData’s delete() and then save() methods on the current managedObjectContext. No problems here.

The problem is actually in the parent view…I won’t show all the code, but will show a simplified version of the LazyVGrid:

LazyVGrid(columns: gridLayout, alignment: .center, spacing: 5) {
                        ForEach(events, id: \.self) { event in
                            HStack {
                                VStack {
                                    Image(uiImage: (event.cardFrontImage ?? blankCardFront)!)
                                        .resizable()
                                        .aspectRatio(contentMode: .fit)
                                        .scaledToFit()
                                        .frame(width: iPhone ? 120 : 200, height: iPhone ? 120 : 200)
                                        .padding(.top, iPhone ? 2: 5)
                                    HStack {
                                        VStack {
                                            Text("\(event.event ?? "")")
                                                .foregroundColor(.green)
                                            Spacer()
                                            HStack {
                                                Text("\(event.eventDate ?? NSDate(), formatter: ViewEventsView.eventDateFormatter)")
                                                    .fixedSize()
                                                    .foregroundColor(.green)
                                                MenuOverlayView(recipient: recipient, event: event)
                                            }
                                        }
                                        .padding(iPhone ? 1 : 5)
                                        .font(iPhone ? .caption : .title3)
                                        .foregroundColor(.primary)
                                    }
                                }
                            }
                            .padding()
                            .frame(minWidth: iPhone ? 160 : 320, maxWidth: .infinity,
                                   minHeight: iPhone ? 160 : 320, maxHeight: .infinity)
                            .background(Color(UIColor.systemGroupedBackground))
                            .mask(RoundedRectangle(cornerRadius: 20))
                            .shadow(radius: 5)
                            .padding(iPhone ? 5: 10)
                        }

As you can see it just displays all the events, where each event is a card. Well since the delete method is in the MenuOverlayView, as defined above. So what happens is when the overlay delete occurs, I should pass back a message to reload the events in the LazyVGrid. But I don’t have that working, as I can’t figure out the correct way to do this.

At least this week, I was able to update my WastedTime in TestFlight to see if I have fixed an issue with the Gauge Complication not displaying the data correctly. So far so goo.

Wasted Time’s Latest Rewrite

As is my habit each summer after WWDC, I rewrite my Wasted Time app to try and take advantage of the latest changes for iOS, macOS, tvOS, and watchOS. This summer has been no difference; however, I also took some time to completely refactor the application to no longer use the AppDelegate feature of older swift programming models. This change was the most dramatic and is still causing me a few problems.

In prior years, the AppDelegate required that I had a different set of code to handle launching on watchOS verses macOS and iOS. I also ended up with lots of compiler directives in order to handle the fact that on watchOS the delegate was a different type than on iOS. This level of #if os(watchOS), really caused a lot of crud in the code. Getting rid of that and using the @State variables, injecting them into the .environment has made things a lot cleaner. It also has helped me fix a lot of long running bugs in my statistics page.

Another challenge I hope to resolve over the course of the year, is removal of @UIApplicationMain and full conversion to @Main in all versions of the app. While I don’t fully have this one worked out in my mind, it is a necessity since my macOS and iPadOS version have broken their keyboard handling. The keyboard was using App Intents, which I have also messed up this summer, and I am sure solving this problem will provide me with improved capabilities for my Widgets.

My widget code has been completely rewritten due to changing over to WidgetKit. I am hoping to make an interactive widget and Live Activity over the course of this year. If I can pull that off I may actually change the version major this year twice!

In the meantime, the investment over the last few years on SwiftUI, along with this summers change to @Main has allowed me to easily create a native VisionOS version of the app. I can’t wait to get my hands on a VisionPro to test it out; but I assume I will not have the opportunity to do so before Apple launches the device early next year.

What’s new in Web Inspector

Note this link – https://developer.apple.com/wwdc23/10118 There is a Safari tech preview I may have to check out for the day job.

  • I always make sure to enable web inspector on my Macs 🙂 
  • You can pair with a device to inspect web content – check out rediscover safari developer features 

Typography inspection

  • Debugging typography issues requires a ton of inheritable actions based on the CSS cascade.  You can see this in the Font panel at the detail view.
  • Sections exist for tons of the font characteristics and features
  • It will show warnings for synthetic styles, like italics.  This can show when you have not loaded the separate italics or bold font file, or the font doesn’t support the character or value you wanted.
    • You can now use variable font files to provide much more flexibility 
    • If you have a variable font the inspector shows all the various variable properties, allowing you to interact with it live to see the results.
  • This whole section sounds like it was designed with Steve Jobs in mind.

User Preference override

  • You can emulate user preferences to check if your design adapts appropriately for user custom settings.  Like reduce motion, screen reader, etc.
    • Right now there are a subset of preferences that can be overridden based on mapping to CSS Media preferences
    • Like dark mode, reduce motion, and increase contrast

Element badges

  • This will demonstrate that elements address CSS badges, like Grid or Flex
    • This will show guides as overlays on the screen
  • To identify unwanted Scrolls there is a new element in the node tree.  This is worse when you have scroll bars turned off.
  • There is a new Event Badge – shows if there is javascript event code attached to elements
    • Popup will show all the listeners attached to the function
    • You can set a breakpoint on the event via this pop-up

Breakpoint enhancements

  • You can run javascript to configure when a breakpoint occurs
  • There are additional types of breakpoints (URL triggers when a network request occurs) and new this year is Symbolic Breakpoint that triggers before a function is about to be invoked.  This seems really cool for website debugging.

Many more features go to webkit.org for more information

The SwiftUI cookbook for focus

Using Focus API’s in SwiftUI

What is Focus

  • This is a tool to decide how to respond when someone interacts with an input method.  On their own they don’t provide enough information on which onscreen control to interact with.
  • When a view has focus, the system will use it as a starting point to react to input 
  • It has a border on macOS, watchOS has a green border, and on tvOS it will hover above the UI
  • Helps users understand where input will go, it’s a special kind of cursor
  • It is a cursor for the user’s attention

Ingredients

  • Focusable views
    • Different views are focusable for different reasons.  Text fields are always focusable.  Buttons are used for clicks and taps, you need keyboard navigation system wide to allow for buttons to be tab able.
    • Buttons support focus for activation.  In iOS 17 and macOS Sonoma – there are new view modifier to define the types of interactions you support.  .focusable(interactions: .edit) or .focusable(interactions: .activate).  If you don’t define an interaction, you will get the .all value. Prior to macOS Sonoma the system only used .activate focus modifier.  (I should add focusable in my code)
  • Focus state
    • The system keeps track of which view has focus – this is @FocusState bindings -it is a bool (or custom data types for more complex interactions)
    • Views can read this to understand when they are in focus or not.
  • Focused values
    • This solves data dependencies that link remote parts of your application.  This is like using a custom environment key and values.  Set up a getter and setter, and define view Modifiers to address this
  • Focus sections
    • Gives you a way to influence how things move when people swipe on a remote or hit tab  on a keyboard.  This follows the layout order of the locale on a keyboard. But directional on Apple TV remote.
    • You use .focusSection() to guide focus to the nearest focusable content.  You will need to add spacers to align content.

Recipes

  • For custom controls you should consider which items to focus on when lists appear.   The Grocery list example in this session explains how to do this with @FocusState and .defaultFocus()
  • If you want the app to move focus programmatically you can use the same example of adding a new item to the list
    • I should address focus in my Wasted Time app for updates on the settings screen
  • If you create a custom control, the custom picker example helps with this pattern.
    • Remember to turn on Keyboard navigation systemwide
  • Focusable grid view is the final receipt – using Lazy Grid

Meet Swift OpenAPI Generator

Swift package plugin to support server API access.  This Dynamic network requests, you need to understand the base, the endpoint and more.

Most services have API documentation, which can be outdated or inaccurate. You can use inspection – but that provides incomplete understanding.

Using formal, structured specification can help reduce these challenges and ambiguities.  This session addresses how apple and swift are support OpenAPI specifications to improve APIs with server apps.

Exploring OpenAPI

  • You can use either YAML or JSON to document the server behavior.  There are tools for testing and generating interactive documentation and spec driven development.
  • There is a lot of boilerplate code you have to write in swift to deal with APIs. So by using OpenAPI you can use tooling to generate most of the boiler plate code.  Here’s a trivial example
  • If we add one optional parameter sec an see that integrated in the Yaml for the Get 
  • The generated code is generated a run time so it is always in sync with the API specification 

Making API calls from your app

  • Sample app is update to replace the sample app to generate one of the cat emoji
  • Here’s the API Specification 
  • The entirety of the code for the API is seen here. Package dependencies are available in the session notes
  • You must configure the generator plugin via the a YAML file
  • Once all the setup is done.. here’s the code:
import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

#Preview {
    ContentView()
}

struct ContentView: View {
    @State private var emoji = "🫥"

    var body: some View {
        VStack {
            Text(emoji).font(.system(size: 100))
            Button("Get cat!") {
                Task { try? await updateEmoji() }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

    let client: Client

    init() {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }

    func updateEmoji() async throws {
        let response = try await client.getEmoji(Operations.getEmoji.Input())

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                emoji = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("cat-astrophe: \(statusCode)")
            emoji = "🙉"
        }
    }
}
  • Pretty simple, huh?

Adapting as the API evolves

  • As the API evolves, you can address the new versions based on app changes based on the new YAML file
  • Adding parameters are also easy to change in your code

Testing your app with mocks

  • Creating a Mock is needed to testing
  • By creating an APIProtocol{} struct you can now mock your code, and make your view generic to utilize the protocol  – this allows for running without a server.  Check out the code at https://developer.apple.com/wwdc23/10171 

Sever development in Swift

  • There is an example of a swift console App to run on the machine.  Using the API Generator simplified the server code requiremnts so you can just focus on the business logic.
  • The demo is using Vapor for the server code
  • The usage of Spec Driven development allows you to focus on your business logic on the server and the OpenAPI generator will automatically generate the stubs for you
  • OpenAPI Generator is a open source project available on GitHub https://github.com/apple/swift-openapi-generator

Meet Assistive Access

This is about addressing cognitive disabilities – distilling apps to their core, you can setup via a parent, guardian or parent

Overview

  • This can be setup via the setting apps, you are guided thru the setup process, including what apps and indicators show.  You can setup in settings or via the Accessibly shortcut.
  • It will provide a different Lock Screen and then a unique Home Screen with larger icons and text, there are five default apps setup to be used with Assistive Access

Principles

  • Design to create an effective experience that reduces cognitive strain.
    • Error prevention and recovery
    • Tasks should be completable without distractions
    • Reduce time dependency on actions
    • Drive consistency

Your App

  • Third party apps will just work.  
  • Large back button on the bottom of the screen, in a reduced frame by default

Optimized for assistive access

  • New info.plist entry – UISupportsFullScreenInAssistiveAccess set to YES if you used adaptive layout ( I should add to Wasted Time)