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)

Keep up with the keyboard

The keyboard has changed a bit over the last few years.  It has new languages, it floats, and it handles multiple screens

Out of process keyboard

  • This is the new architecture – allows for improve privacy and security
  • This is now a process out side of your app, this will be an asynchronous process with the system and your app requesting updates from each other. Ultimately you will get a Text Insertion – it may provide some slight timing issues
  • Frees memory from your app
  • Provides flexibility for future changes 

Design for the keyboard

  • Note in the old model, you moved your view up to address for the keyboard.  However in the new model, you need to adjust your app to the intersection with the keyboard overlay (like in Stage manager).  You may have multiple scenes that have to adjust.
  • If you use the mini-toolbar, in stage manager is different than out of stage manager.
  • Keyboard layout guide was introduced in iOS 15 for UIKit, this has been updated by iOS apps, and is the recommended way to address the keyboard.
    • view.keyboardLayoutGuide.topAnchor.constraint(equalTo: textView.bottomAnchor).isActive = true
    • This has been updated to allow for more customization on iOS 17
  • SwiftUI automatically handles the common cases for you, by adjusting the safe view.
  • Notifications
    • In the past you had to listen for will show, did show, etc. and then process these yourself.  But with Stage manager introduction those patterns didn’t work
    • It certainly seems that many of these changes are why Stage Manager only got as far as it did last year.

New text entry APIs

  • Inline predictions will allow for on device processing and context to provide information.  This is enabled by default for most text fields.  This is powerful

Embed the Photo Picker in your App

The new photo picker, you don’t need request any permissions to use it.  It is only a few lines of code to use this.

Embedded Picker

  • The access model runs in a separate model outside of your app. Only what is selected is passed back to your app.
    • You can use the new .photosPickerDisabledCapabilities modifier to turn off certain features of the picker
    • You can disable various accessories to with .photosPickerAccessoryVisibility modifier
    • You can even change the size of the picker
    • You can use the new .photosPickerStyle(.inline) to make it more naturally a part of your app
    • PhotosPicker(selectionBehavior: .continuous) allows your app to respond to each selection of an image.
  • There’s a new privacy badge on the picker
  • A detailed scission of which options can be disabled in the sessions including search, selection actions, and more
  • The picker style include presentation, inline and compact style (a single row – scrolling horizontally)
  • This API is available for iOS, iPadOS, macOS, along with SwiftUI, AppKit and UIKit – it was not listed for xrOS or VisionOS

Options Menu

  • This is a new menu, which gives users control of what is shared with your app.  They can select to remove metadata and location as example.

HDR and Cinematic

  • The system may automatically transform assets to things like JPEF, but if you want to include HDR data
    • you need so set .current encoding policy
    • And use .image or .more for content type