Concurrency, Closures, and SwiftUI on Mac OS X

Synopsis

I want to get some clarity about how to use GCD and closures to handle dispatching work between multiple contexts. This is something I’m very familiar with in Java, but not at all in Swift (let alone, using SwiftUI). Inspired by a cartoon that appeared in The New Yorker many years ago, Barkin’ Dogs represents the conversation that happens when two dogs get excited.

What It Looks Like

We’ll just have a single window with a button to poke one of the dogs, and a scrolling view that shows the ongoing conversation. Initially, I’d wanted to make it look kind of like a conversation in Messages, where each new message appears at the bottom, but the SwiftUI affordance to let us make a ScrollView scroll to the end, ScrollViewReader, only became available with Mac OS 11 (Big Sur) and I’m not ready to go there, yet. (I tried, and the upgrade process wedged my computer so that I killed a day restoring from a backup. Also, the default appearance of Big Sur makes my eyes bleed.) So, the new messages will appear at the top of the list, pushing older messages down.

Data Model

Clearly, we’re going to be trafficking in messages, so there needs to be some kind of construct to represent one of these things. Also, these messages get generated by different actors, so we’re going to want something to represent a message source – a dog.

Procedure

Fire up Xcode and create a new project.

  • select ‘macOS’, and ‘App’
  • on the next screen, for "Interface" select "SwiftUI", for "Life Cycle" select "AppKit App Delegate", and leave the Core Data and Tests checkboxes unchecked
  • name the project whatever you want; I chose "Barkin’ Dogs" because it’s silly

Create a Message type

  • create a new Swift file named, Message
  • change the top import from Foundation to SwiftUI so that we can have a Color stored in the message
  • we don’t need a class since, once it has been created, a message doesn’t change. We are going to be iterating over a list of them, though, so it should conform to Identifiable. I have come, through developing distributed systems in other languages, to love UUID as a unique identifier that is easy to pass between systems running on different hardware, different operating systems, and different languages. So, rather than using a hash or a timestamp or a simple integer, I’ll use a UUID as the identifier. Here is the code for the message:
import SwiftUI

struct Message: Identifiable {
    let id: UUID
    let message: String
    let color: Color?

    init(_ msg: String, color: Color? = nil) {
        id = UUID.init()
        message = msg
        self.color = color
    }

    static var example: Message {
        Message("Example message")
    }

    static var examples: [Message] {
        var sampleRA = [Message]()
        for i in 1..<11 {
            sampleRA.append(Message("Sample message \(i)"))
        }
        return sampleRA
    }
}

Next, we need to create a dog entity to create some messages.

  • As with Message, create a new Swift file, naming this one, "Dog".
  • Replace the Foundation import with SwiftUI, since we’re going to want to store the dog’s color on the dog – that way it can apply its color to all the messages it barks out.
  • A dog doesn’t have any mutable state, so it doesn’t need to be a class; it can get away with being a struct.
  • It does need a method to receive a message and emit one back out in response. If the dog is poked, it will say, "Hey!" If someone tells the dog, "Hey!" then it will respond, "Shut up!" And if someone tells the dog, "Shut up!" it will respond, "No, you shut up!"
  • Here’s the code for dog:
import SwiftUI

struct Dog: Identifiable {
    let id = UUID()

    var name: String = "Anonymous Dog"
    var color: Color = Color(white: 0.5, opacity: 0.2)

    func respond(to message: Message) -> Message {
        var response: Message
        if (message.message == "<Poke>") {
            response = Message("Hey!", color: color)
        } else if (message.message == "Hey!") {
            response = Message("Shut up!", color: color)
        } else {
            response = Message("No, you shut up!", color: color)
        }
        return response
    }
}

Keep track of the dogs

We need something to keep track of which dog has spoken and which dog is listening. I started out with this being properties of ContentView, but the UI doesn’t strike me as the right place to keep track of the state of the model. I decided to put the logic and data for keeping track of the dogs and their barking into a Kennel object. The kennel creates the dogs and keeps them in a list. When a message comes in, it gets sent to the first dog in the list, who then goes to the end of the list. This is also where we bring in GCD. Not for parallel computing, but to simulate networked systems talking to each other. Here’s the code for the kennel; note that here, I’ve declared it as a class, since the list of dogs mutates over time.

import SwiftUI

class Kennel {
    var dogs: [Dog]

    init() {
        dogs = [Dog]()
        dogs.append(Dog(name: "Ginger", color: .green))
        dogs.append(Dog(name: "Chewie", color: .orange))
    }

    func receive(message: Message, responseHandler: @escaping (Message) -> Void) {
        let barker = dogs.removeFirst()
        dogs.append(barker)

        DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) {
            responseHandler(barker.respond(to: message))
        }
    }
}

Display messages

Ultimately, we’re going to be drawing these messages on the screen. I’m lazy, but I know that at some point I’m going to want to make this look good. It’ll be funnier if it really looks like a chat window from the Messages app. For now, I’m just making the messages draw the text of the message on a color background, but applying a rounded rectangle clip to them and then a little curly tail to point to one side or the other like the speech bubbles in Messages will be a lot easier to fiddle with if I stick the message drawing into its own view. So, MessageView:

import SwiftUI

struct MessageView: View {
    let message: Message

    init(_ msg: Message) {
        message = msg
    }

    var body: some View {
        ZStack {
            if let c = message.color {
                c
            }
            Text(message.message)
        }
    }
}

struct MessageView_Previews: PreviewProvider {
    static var previews: some View {
        MessageView(Message("Sample Message", color: .red))
    }
}

ContentView — finally!

Here we go, with the main display of the application. It’s super basic: a button to kick off the barking storm, and a scrolling view to display all the messages as they get barked out. The only even slightly interesting part is where the kennel gets passed in to the initializer.

import SwiftUI

struct ContentView: View {
    private let kennel: Kennel

    @State private var messages = [Message]()

    init(_ dogs: Kennel) {
        kennel = dogs
    }

    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button("Poke Dog") {
                    add(message: Message("<Poke>"))
                }
            }
            .padding()
            ScrollView {
                ForEach(messages, content: MessageView.init)
            }
        }
    }

    func add(message: Message) {
        // Once we're in Big Sur, we can use append, since then we can use a ScrollViewReader
        messages.insert(message, at: 0)
        // now, the message has come in, so it's time for someone to bark
        kennel.receive(message: message) { response in
            add(message: response)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(Kennel())
    }
}

The Final Setup

In the generated AppDelegate, we need to modify the line where we create the ContentView, passing in a kennel:

        let contentView = ContentView(Kennel())

…and, because I like being explicit about quitting the application when the window closes (after all, it’s not a document based app), I add this override to AppDelegate as well:

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }

Published by pirateguillermo

I play the bagpipes. I program computers. I support my family in their various endeavors, and I enjoy my wonderful life.

Leave a Reply

Discover more from Mechadarwin

Subscribe now to keep reading and get access to the full archive.

Continue reading