Chatter SwiftUI

Cover Page

DUE Wed, 09/18, 2 pm

In this lab you’ll learn how to retrieve textual chatts from a back-end server and how to post to it. You will familiarize yourselves with the iOS app development environment and the basics of the platform. You’ll also learn some Swift syntax and language features that may be new to you. And you will use SwiftUI to build UI declaratively. Let’s get started!

This lab can be completed in the iOS simulator.

Expected behavior

Posting a new chatt:

DISCLAIMER: the video demo shows you one aspect of the app’s behavior. It is not a substitute for the spec. If there are any discrepancies between the demo and the spec, please follow the spec. The spec is the single source of truth. If the spec is ambiguous, please consult the teaching staff for clarification.

Be patient, the Chatter app on your device or simulator will be very slow because we’re running on debug mode, not release mode. It could be several seconds after launch for the icons and messages shown on the video to show up.

Preliminaries

If you don’t have an environment set up for iOS development, please read our notes on Getting Started with iOS Development first.

Before we start, you’ll need to prepare a GitHub repo to submit your labs and for us to communicate your lab grades back to you. Please follow the instructions in Preparing GitHub for EECS 441 Labs and then return here to continue.

Creating an Xcode project

In the following, please replace <YOUR UNIQNAME> with your uniqname. Apple will complain if your Bundle Identifier is not globally unique. Using your uniqname is one way to generate a unique Bundle Identifier.

Depending on your version of Xcode, the screenshots in this and subsequent lab specs may not look exactly the same as what you see on screen.

  1. Click Create a new Xcode project in “Welcome to Xcode” screen (screenshot)
  2. Select iOS > App and click Next (screenshot)
  3. Enter Product Name: swiftUIChatter
  4. Team: None

    if you don’t have one yet, otherwise choose your Personal Team

  5. Organization Identifier: edu.umich.<YOUR UNIQNAME> 👈👈👈

    replace <YOUR UNIQNAME> with yours, remove the angle brackets,< >

  6. Interface: SwiftUI
  7. Language: Swift
  8. Leave the boxes unchecked, click Next
  9. On the file dialog box that pops up, put your swiftUIChatter folder in 👉👉👉 YOUR*LABS*FOLDER/chatter/swiftUIChatter/, where YOUR*LABS*FOLDER is the name you give to your 441 GitHub repo clone folder above.
  10. Leave Create Git repository on my Mac UNCHECKED (screenshot). We will add the files to GitHub using GitHub Desktop instead. And leave Add to to the default, “Don’t add to any project or workspace”.
  11. Create Create

Once the project is created, navigate to your project editor (top line of Xcode left pane showing your Product Name). Xcode will then show the General settings for your project in its middle pane. In the Identity section, confirm that your Bundle identifier is edu.umich.<YOUR UNIQNAME>.swiftUIChatter. Apple will complain if your Bundle Identifier is not globally unique.

Next in the Minimum Deployments section, using the drop-down selector, choose the iOS version that your whole team is running.

If you selected None for Team when creating your project above, click on the Signing & Capabilities tab (up top, next to the General tab). You will need to specify a Team. If you don’t yet have a Personal Team, please create one now (for free) using your Apple ID. In the drop down menu next to Team select Add an Account... at the bottom of the menu, sign in using your Apple ID and follow the prompts to create one.

Checking GitHub

Open GitHub Desktop and

If you are proficient with git, you don’t have to use GitHub Desktop. However, we can only help with GitHub Desktop, so if you use anything else, you’ll be on your own.

:point_right: Go to the GitHub website to confirm that your folders follow this structure outline:

  441
    |-- chatter
        |-- swiftUIChatter
            |-- swiftUIChatter.xcodeproj
            |-- swiftUIChatter

If the folders in your GitHub repo does not have the above structure, we will not be able to grade your labs and you will get a ZERO.

Xcode project structure

The left or Navigator pane of your Xcode window should put your project files under swiftUIChatter project (top-line), in a swiftUIChatter folder:

One of the main challenges in learning to develop apps declaratively is unfamiliarity with the platform’s UI framework. What are all the available UI elements? How do we create them? What modifiers are there? What are their signatures? Everything is documented of course, but if you don’t know what’s available, if you don’t know the name of the Views and modifiers to look up, how do you search for it? Click on the + sign at the upper right corner of the Xcode window, at the very top menu bar, a “View library browser” will pop up with detailed explanation of each View and Modifier (screenshot).

Previews

The #Preview feature is used by Xcode only during development, to preview your View(s). If the preview pane is not showing, you can toggle it by checking Canvas on the Adjust Editor Options menu on the top right corner of your Xcode window (screenshot). The preview only renders your View, it is not a simulator, it won’t run non-UI related code. Given the small sizes of our labs, I found the preview to be of limited use and rather slow and would therefore just comment out the #Preview feature, which automatically disables the preview and closes the Canvas pane.

While we can put all of our Views in one file, it can get quite unwieldy scrolling through the large file in latter labs. So we will put each View in its own file. While not an industry convention, in this course we name the initial View MainView.

You will notice single characters, such as ‘A’, ‘M’, ‘R’ showing up next to your source files. These indicate the file version control status, also used with git.

While SwiftUI has been gaining in popularity and has become more mature since its introduction in 2019, compared to UIKit, it is still lacks some UIKit features such as certain advanced animations and visual effects.

Chatter

Consists of two views: one to write and post a chatt to the server, and the other, the main view, to show retrieved chatts. It is cheaply inspired by Twitter. And it laready has a live web API:

https://mada.eecs.umich.edu/getchatts/
https://mada.eecs.umich.edu/postchatt/

You will create your own back end later in the lab.

Chatt

To post a chatt with the posttchatt API, Chatter backend server expects a JSON object consisting of “username” and “message”. For example:

{	
   "username": <YOUR UNIQNAME>,	
   "message": "Hello world!"	
}

Chatter’s getchatts API will send back all accumulated chatts in the form of a JSON array of string arrays. Each string array consists of four elements “username”, “message”, “id”, and “timestamp”. For exmaple:

[
    ["username0", "message0", "id0", "timestamp0"],
    ["username1", "message1", "id1", "timestamp1"], 
    ... 
]

Each element of the string array may have a value of JSON null or the empty string ("").

Create a new Swift file:

  1. Right click on the swiftUIChatter folder on the left/navigator pane
  2. Select New File...
  3. Choose an iOS > Swift File template and click Next.
  4. For Save As: enter the name Chatt and click Create.
  5. Replace the import Foundation line in the file with the following struct definition for Chatt:
import Foundation

struct Chatt: Identifiable {
    var username: String?
    var message: String?
    var id: UUID?
    var timestamp: String?
    var altRow = true

    // so that we don't need to compare every property for equality
    static func ==(lhs: Chatt, rhs: Chatt) -> Bool {
        lhs.id == rhs.id
    }    
}

We declare the Chatt struct as conforming to the Identifiable protocol, which simply means that it contains an id property that SwiftUI can use to uniquely identify each instance in a list. We use randomly generated UUID to identify each chatt. Since multiple front-end apps can post chatts to the same back end, the UUID generation will be done by the back end as it enters each chatt into its database. We also provide a == operator to equate two instances as long as they have the same id. The altRow property is used to alternate the background color of the entries when displayed in a list.

ChattStore as Model

We will declare a ChattStore object to hold our array of chatts. Since the chatts are retrieved from and posted to the same Chatter back-end server, we will keep the network functions to communicate with the server as methods of this class.

Create another Swift file, call it ChattStore, and place the following ChattStore class in it:

import Observation

@Observable
final class ChattStore {
    static let shared = ChattStore() // create one instance of the class to be shared
    private init() {}                // and make the constructor private so no other
                                     // instances can be created

    private var isRetrieving = false
    private let synchronized = DispatchQueue(label: "synchronized", qos: .background)

    private(set) var chatts = [Chatt]()
    private let nFields = Mirror(reflecting: Chatt()).children.count-1

    private let serverUrl = "https://mada.eecs.umich.edu/"
}

Once you have implemented your own back-end server, you will replace mada.eecs.umich.edu with your server’s IP address.

The first two declarations in ChattStore make it a singleton object, meaning that there will ever be only one instance of this class when the app runs. Since we want only a single copy of the chatts data, we make this a singleton object. By Swift’s convention, the singleton instance is stored in its shared property.

We annotate the ChattStore class with the @Observable macro (part of the Observation package) to publish its public properties for subscription. When a SwiftUI View subscribes to a published observable variable (the subject), it will be notified and the View will be recomputed and re-rendered automatically as necessary. The chatts array will be used to hold chatts retrieved from the back-end server. While we want chatts to be readable outside the class, we don’t want it publicly modifiable, and so have set its “setter” to be private. We use the variable isRetrieving to ensure that each front end can have only one on-going back-end retrieval at any one time. We will serialize access to isRetrieving using the synchronized dispatch queue in getChatts() later.

The code Mirror(reflecting: Chatt()).children.count uses introspection to obtain the number of properties in the Chatt type. We store the result (-1 to discount the altRow property used only on the front end) in the variable nFields for later validation use.

ChattListRow

We want to display the chatts retrieved from the backend in a timeline view. First we define what each row contains. Create a new SwiftUI file , ChattListRow.

When creating a new file, Xcode also gives the option to create a SwiftUI file. If you find the SwiftUI Preview feature useful, you can choose to create a SwiftUI file instead.

Replace the import Foundation line in the file with:

import SwiftUI

struct ChattListRow: View {
    let chatt: Chatt
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                if let username = chatt.username, let timestamp = chatt.timestamp {
                    Text(username).padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)).font(.system(size: 14))
                    Spacer()
                    Text(timestamp).padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)).font(.system(size: 14))
                }
            }
            if let message = chatt.message {
                Text(message).padding(EdgeInsets(top: 8, leading: 0, bottom: 6, trailing: 0))
            }
        }
    }
}

When we declare a struct as conforming to View, it is required to have a variable called body of type some View. The property body is where you describe your View: which UI elements will be included, how they relate to each other positionally, e.g., one above the other? or side by side? The keyword some here means that the actual type will be determined at compile time, depending on actual usage, and it can be any type that conforms to View (we’ll learn more about some when discussing generics later in the term).

The content of a ChattListRow View consists of a vertical stack of two items: a horizontal stack on top and, below it, a text box containing the chatt message. The chatt message is padded so that the message is displayed inside the textbox, away from the top and bottom edges of the textbox.

The horizontal stack consists of three items: a text box flushed to the leading edge containing the username and another text box flushed to the trailing edge containing the timestamp. The contents of these boxes are padded at their top edges only. Between these two text boxes we place a Spacer. When placed in an HStack, a Spacer fills out the space horizontally. Conversely, in a VStack it fills out the space vertically. In this case, the username and timestamp textboxes will each take up as much space as needed by their contents. The Spacer will then occupy all the space in between such that the two textboxes can be flushed left and right respectively.

If your locale has a language that reads left to right, leading is the same as left, otherwise for languages read right to left, leading is the same as right (conversely and similarly trailing). Most of the time you would use leading and trailing to refer to the two ends of a UI element, reserving left and right to the physical world, e.g., when giving direction.

You can option-click (⌥-click) on a View (e.g., VStack, Text, or NavigationStack) to bring up a menu of possible actions on it. The Show SwiftUI Inspector menu item allows you to visually set the paddings, for example. The inspector is also accessible directly by ctl-option-click (⌃⌥-click), bypassing the menu.

DSL

Notice how type inference and the use of trailing closure makes HStack, VStack, Text, Spacer, etc. look and act like keywords of a programming language used to describe the UI, separate from Swift. Hence SwiftUI is also considered a “domain-specific language (DSL)”, the “domain” in this case being UI description.

MainView

In your MainView file, let’s start with showing chatts in a timeline in the MainView struct:

struct MainView: View {
    private let store = ChattStore.shared

    var body: some View {
        List(store.chatts) {
            ChattListRow(chatt: $0)
                .listRowSeparator(.hidden)
                .listRowBackground(Color($0.altRow ?
                    .systemGray5 : .systemGray6))
        }
        .listStyle(.plain)
    }
}

To present a view for each element of a list, we can use the SwiftUI List struct. For each element in a list, ChattListRow constructs and returns a View, which List can then display. Recall that we have previously tagged ChattStore an @Observable. When a View accesses ChattStore’s property chatts, SwiftUI automatically subscribes the View to ChattStore so that it can be recomputed and re-render automatically, as necessary, when chatss is modified. See also the sidebar on @State later.

Alternative to List

We could use ForEach wrapped in a LazyVStack instead of List (both LazyVStack and List only load list elements that are in view). List, hoever, comes with a number of convenient built-in list operations such as selecting, moving, and deleting entries (see documentation for usage).

getChatts()

If you run your app now, you will get a blank screen as we have no means to retrieve and populate the chatt timeline yet.

Let us return to ChattStore and add the following getChatts() method to the class, to retrieve chatts from the back-end server:

    func getChatts() {
        // only one outstanding retrieval
        synchronized.sync {
            guard !self.isRetrieving else {
                return
            }
            self.isRetrieving = true
        }

        guard let apiUrl = URL(string: "\(serverUrl)getchatts/") else {
            print("getChatts: Bad URL")
            return
        }
        
        DispatchQueue.global(qos: .background).async {
            var request = URLRequest(url: apiUrl)
            request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Accept") // expect response in JSON
            request.httpMethod = "GET"

            URLSession.shared.dataTask(with: request) { data, response, error in
                defer { // allow subsequent retrieval
                    self.synchronized.async {
                        self.isRetrieving = false
                    }
                }            
                guard let data = data, error == nil else {
                    print("getChatts: NETWORKING ERROR")
                    return
                }
                if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
                    print("getChatts: HTTP STATUS: \(httpStatus.statusCode)")
                    return
                }
                
                guard let chattsReceived = try? JSONSerialization.jsonObject(with: data) as? [[String?]] else {
                    print("getChatts: failed JSON deserialization")
                    return
                }
                var idx = 0                
                var _chatts = [Chatt]()
                for chattEntry in chattsReceived {
                    if chattEntry.count == self.nFields {
                        _chatts.append(Chatt(username: chattEntry[0],
                                                message: chattEntry[1],
                                                 id: UUID(uuidString: chattEntry[2] ?? ""),
                                                 timestamp: chattEntry[3],
                                                 altRow: idx % 2 == 0))
                        idx += 1
                    } else {
                        print("getChatts: Received unexpected number of fields: \(chattEntry.count) instead of \(self.nFields).")
                    }
                }
                self.chatts = _chatts
            }.resume()
        }
    }

We first check whether the app is already in the middle of retrieving chatts from the back end. If not, we can proceed. Checking and updating isRetrieving is serialized using the synchronized dispatch queue.

To retrieve chatts, we create a URLRequest(url:) with the appropriate GET URL, which we send to the back-end server in a URLSession task. To prevent iOS from complaining about thread priority inversion, we explicitly run the URLSession task with .background quality-of-service (qos).

By default URLRequest() doesn’t tell the back-end server the request type. The Go server, on the other hand, requires the type of the request to be specified.

The server will return the chatts as a JSON object. In the completion handler to be invoked when the response returns, we call .JSONSerialization.jsonObject(with:) to decode the serialized JSON array from the returned response. We update the chatts array to hold the chatts in the returned response. After the chatts array has been fully updated, we update the isRetrieving property to allow subsequent retrieval.

Once the URLSession task is created, we call the task’s .resume() method to submit it to the NSOperationQueue of URLSession for asynchronous execution.

Swift Codable

To {en,de}code between JSON and Swift data, we could use the Swift Codable protocol. Swift Codable works well with JSON objects (“key”: “value” pair), but requires manual parsing for array of unkeyed values, used in our labs. Programming Swift Codable’s recursive descent parser declaratively to decode unkeyed data, potentially mixed with further nested unkeyed data (i.e., array of String or [String] as in the Maps lab) is more complicated than decoding the JSON data directly, imperatively. Use of property wrapper in subsequent labs require further programming of Swift Codabe parser.

To retrieve chatts from the back end on application launch, add the following to your swiftUIChatterApp struct before the declaration of var body:

    init() {
        ChattStore.shared.getChatts()
    }

Pull down to refresh

Now that we have a means to retrieve chatts from the back-end server, we return to MainView to retrieve the chatts and use them to populate the List view. Add the following code below the List {}.listStyle(.plain) block in your MainView.body:

            .refreshable {
                store.getChatts()
            }

Running your app now will still show a blank screen, but if you pull down on the screen (tap and drag down with your finger), the screen should refresh and show a timeline of retrieved chatts.

Note that refreshable() in this case doesn’t actually refresh the view, it calls getChatts() which refreshes the chatts array. SwiftUI then re-renders the view given the updated chatts array.

When the user posts a chatt, we will automatically call getChatts() to refresh our chatts array. We implement .refreshable() to refresh our chatts array with postings from other users. Since our backend doesn’t implement a push mechanism, such as websockets, the user must actively pull to refresh.

PostView

We are not done with MainView, but let us put it aside for awhile and shift our focus to PostView, which we will use to compose and post a chatt. Create a new Swift file, call it PostView, and replace the import Foundation line with:

import SwiftUI

struct PostView: View {
    @Binding var isPresented: Bool

    private let username = <YOUR UNIQNAME>
    @State private var message = "Some short sample text."
    
    var body: some View {
        VStack {
            Text(username)
                .padding(.top, 30.0)
            TextEditor(text: $message)
                .padding(EdgeInsets(top: 10, leading: 18, bottom: 0, trailing: 4))
                .frame(minHeight: 80, maxHeight: 120)
        }
    }
}

We describe PostView as a vertical stack of two UI elements: a text box displaying the immutable variable username (replace <YOUR_UNIQNAME> above with yours, use quotation marks) and an editable text box bound to the @State private variable message. The variable isPresented is used to navigate between MainView and PostView, which we’ll discuss in the next section.

The annotation @State is used to enable observation of “view-logic” states, i.e., variables allocated in SwiftUI memory. Instantiating a child View with a parent’s @State property automatically subscribes the child to the @State property. For read-only subscription, declare the child property immutable (let). For read-write two-way binding, annotate the child’s property with @Binding, allowing the child subscriber to modify the state. When an @State property changes, any child View subscribed to the property will be recomputed and, if necessary, re-rendered.

The $ used in front of a @Binding or @State property accesses the binding of (pointer or reference to) the property [like the & in C/C++].

Or rather, it gives access to the projectedValue of the wrapped property; in this case, the projectedValue is the binding for the property.

</details>

Subscribing to @Observable class gives two-way bindings to its properties. However, if it’s necessary to further pass along the binding to another child View using the $ prefix, such as when calling TextField(text:), the subscriber property must be tagged with @Bindable (or @Binding).

no import Observation

While @Observable is a macro defined in the Observation package, both @State, @Binding, and @Bindable are property wrappers defined in the SwiftUI package, hence we don’t need import Observation to use them.

</div>

In the body of swiftUIChatterApp, in the file swiftUIChatterApp.swift, inside the WindowGroup{} block, replace the call to ContentView() with:

            NavigationStack {
                MainView()
            }

which put our MainView() a navigation stack, allowing us to move back and forth between MainView() and its child Views. NavigationStack() also put a navigation toolbar with a button on top of MainView and its child Views, with a back button on the top left of the navigation toolbar of child Views.

We return to MainView to set up navigation between MainView and PostView and to add a button that takes us to PostView when tapped. We want the button at the top right corner of the view, within the navigation toolbar.

To enable presenting PostView, add the following property to the MainView struct, before var body:

    @State private var isPresenting = false

And add the following inside the body block, after the call to refreshable:

            .navigationTitle("Chatter")  
            .navigationBarTitleDisplayMode(.inline)          
            .toolbar {
                ToolbarItem(placement:.navigationBarTrailing) {
                    Button { 
                        isPresenting.toggle() 
                    } label: {
                        Image(systemName: "square.and.pencil")
                    }
                }
            }
            .navigationDestination(isPresented: $isPresenting) {
                PostView(isPresented: $isPresenting)
            }            

The first two lines set the title of the view, centered on the navigation bar. The next line places a button with the icon square.and.pencil on the navigation bar, flushed right. When tapped, the button toggles the property isPresenting (from false to true).

The call to navigationDestinaton(isPresented:destination:) checks that isPresented is true, and if so, presents the destination View. The argument label isPresented provides binding for MainView’s property, isPresenting, as indicated by the $ in front of isPresenting. When we declared PostView earlier, we also created an isPresented property with the @Binding annotation, which allows us to now pass along the binding for isPresenting from MainView to PostView as isPresented. Due to the two-way binding afforded by the use of @Binding, when we toggle the value of isPresented in PostView, the change will be propagated back to the isPresenting property in MainView. Further, when isPresenting changes to false, navigationDestination(isPresented:destination:) sees that its isPresented is now false and dismisses the View.

With that, we’re done with MainView! Let’s finish up PostView.

Posting chatts

As we have done in MainView, we’d like to give the view a title and a button at the upper right corner to post a chatt. In the body of PostView, add the following modifiers to the VStack { } block:

        .navigationTitle("Post")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement:.navigationBarTrailing) {
                SubmitButton()
            }
        }

As before, we set a title for the view, centered on the navigation bar. Then we create a button to post chatt. However, instead of describing the button inline, as we did in MainView, we put it in a separate View, SubmitButton(), so that we can more easily refer to it when we want to make changes in latter labs. Add the following code inside your PostView struct:

    @ViewBuilder
    func SubmitButton() -> some View {
        @State var isDisabled: Bool = false

        Button {
            isDisabled = true
            ChattStore.shared.postChatt(Chatt(username: username, message: message)) {
                ChattStore.shared.getChatts()
            }
            isPresented.toggle()
        } label: {
            Image(systemName: "paperplane")
        }
        .disabled(isDisabled)
        .opacity(isDisabled ? 0.2 : 1)        
    }

Instead of creating a View struct, we use the alternate method to create a View, by using @ViewBuilder function. The advantage in this case is that the function declared inside the PostView struct can access properties of PostView, such as isPresented, whereas a nested struct would not have been able to. Apple has indicated that any performance difference between the two methods are negligible. We give the button a paperplane icon. When tapped, the button calls postChatt(_:) with a Chatt object instantiated with the values of username and message properties of PostView. Once the user has clicked the Send button, we set isDisabled to true to disable and “grey out” the button. This feature is not as visible in this lab since the sending process completes immediately. In latter labs, when the sending process can take some time, it prevents user from repeatedly clicking the SubmitButton.

Once the chatt is posted, we call getChatts() to retrieve an updated list of chatts from the back end, including chatts other users may have posted since our previous call to getChatts(). Thanks to reactive UI, MainView will update its displayed timeline automatically when the chatts array in ChattStore is updated, without the user having to pull down to refresh.

To ensure the call to getChatts() occurs only after postChatt(_:) has completed (not just that the chatt is sent, but that a response has returned from the sender), we provide a completion function to postChatt(_:) as a trailing closure. We call getChatts() in this trailing closure. In the definition of postChatt(_:) below, we see that the completion closure is run in the callback function of URLSession.shared.dataTask(with:), which is executed when URLSession.shared.dataTask(with:) is done posting the chatt.

Once we fire off postChatt(_:), we toggle the property isPresented, which causes navigationDestination(isPresented:destination:) in MainView to see that its isPresented parameter is now false and consequently dismisses PostView. Depending on the amount to be sent and the state of the network, postChatt(_:) may not have completed when we return to MainView.

We now provide the postChatt(_:) method.

postChatt(_:)

Add the postChatt(_:) method to the ChattStore singleton:

    func postChatt(_ chatt: Chatt, completion: @escaping () -> ()) {
        let jsonObj = ["username": chatt.username,
                       "message": chatt.message]
        guard let jsonData = try? JSONSerialization.data(withJSONObject: jsonObj) else {
            print("postChatt: jsonData serialization error")
            return
        }
                
        guard let apiUrl = URL(string: "\(serverUrl)postchatt/") else {
            print("postChatt: Bad URL")
            return
        }
        
        DispatchQueue.global(qos: .background).async {
            var request = URLRequest(url: apiUrl)
            request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
            request.httpMethod = "POST"
            request.httpBody = jsonData

            URLSession.shared.dataTask(with: request) { data, response, error in
                guard let _ = data, error == nil else {
                    print("postChatt: NETWORKING ERROR")
                    return
                }

                if let httpStatus = response as? HTTPURLResponse {
                    if httpStatus.statusCode != 200 {
                        print("postChatt: HTTP STATUS: \(httpStatus.statusCode)")
                        return
                    } else {
                        completion()
                    }
                }

            }.resume()
        }
    }

In postChatt(_:), we first assemble together a Swift dictionary comprising the key-value pairs of data we want to post to the server. To post it, we create a URLRequest() with the appropriate POST URL. We can’t just post the Swift dictionary as is though. The server may not, and actually is not, written in Swift, and in any case could have a different memory layout for various data structures. Presented with a chunk of binary data, the server will not know that the data represents a dictionary, nor how to reconstruct the dictonary in its own dictionary layout. To post the Swift dictionary, therefore, we call JSONSerialization.data(withJSONObject:) to encode it into a serialized JSON object that the server will know how to parse. We next create a URLSession task with the POST request and call the task’s .resume() method to submit it to the NSOperationQueue of URLSession for asynchronous execution. Again, to prevent iOS from complaining of thread priority inversion, we explicitly run URLSession with .background qos.

If there was no error in posting the chatt, we call the provided completion() closure.

completion

We designed getChatts() and postChatt(_:) to handle all networking aspects of the labs, including conversion between JSON to Swift data. The function getChatts() grabs the current ensemble of chatts from the back end, convert each from JSON to Swift data, and store them in the ChattStore.chatts array. Thanks to reactive UI, MainView will update its displayed timeline automatically when the chatts array in ChattStore is updated, without the user having to pull down to refresh. While the Chatt format may be customized to each lab, there is no further lab-specific cleanup functions after the chatts are stored, hence we don’t need to pass any customized completion function to getChatts().

On the other hand, postChatt(_:) grabs the chatt to be posted, in the form of Swift data, converts it to a JSON package, and sends it to the back end. After postChatt(_:) has completed posting a chatt, we want to call getChatts() to retrieve the current batch of chatts from the back end. To ensure that getChatts() will return our recently posted chatt, we call it only after postChatt(_:) has completed. We may also need to do some lab-specific cleanup, mainly releasing and resetting various memory resources that should only be done when postChatt(_:) is no longer dependent on them. We ensure that these steps are performed in succession by passing a lab-specific completion() closure when calling postChatt(_:), to be run only after postChatt(_:)’s network operation has completed.

Congratulations! You’re done with the first lab!

Run and test to verify and debug

You should now be able to run your front end against the provided back end on mada.eecs.umich.edu.

If you’re not familiar with how to run and test your code, please review the instructions in the Getting Started with iOS Development.

Completing the back end

Once you’re satisfied that your front end is working correctly, follow the back-end spec to build your own back end:

With your back end completed, return here to prepare your front end to connect to your back end via HTTPS.

Installing your self-signed certificate

Download a copy of your chatterd.crt to YOUR*LABS*FOLDER on your laptop. Enter the following commands:

laptop$ cd YOUR*LABS*FOLDER
laptop$ scp -i eecs441.pem ubuntu@YOUR_SERVER_IP:441/chatterd.crt chatterd.crt

Install your chatterd.crt onto your iOS:

On iOS simulator

Drag chatterd.crt on your laptop and drop it on the home screen of your simulator. That’s it!

To test the installation, launch a web browser on the simulator and access your server at https://YOUR_SERVER_IP/getchatts/.

On iOS device

AirDrop chatterd.crt to your iPhone or email it to yourself.

Then on your device:

WARNING: DO ALL 10 STEPS: IT IS A COMMON ERROR TO MISS THE LAST THREE STEPS!

  1. If you AirDrop your chatterd.crt, skip to next step. If you emailed the certificate to yourself, view your email and tap the attached chatterd.crt.

    If you don’t using Apple’s Mail app on your iPhone, you may have to “share” the cert and choose Save to Files, then launch the Files app on your phone and in the Downloads folder locate your chatterd.crt and tap it.

  2. You should see a Profile Downloaded dialog box pops up.
  3. Go to Settings > General > VPN & Device Management and tap on the profile with YOURSERVERIP.
  4. At the upper right corner of the screen, tap Install.
  5. Enter your passcode.
  6. Tap Install at the upper right corner of the screen again.
  7. And tap the somewhat dimmed out Install button.
  8. Tap Done on the upper right corner of screen.
  9. :point_right:Go back to Settings > General
  10. :point_right:Go to [Settings > General >] About > Certificate Trust Settings
  11. :point_right:Bravely slide the toggle button next to YOURSERVERIP to enable full trust of your CA’s certificate and click Continue on the dialog box that pops up

To test the installation, launch a web browser on your device and access your server at https://YOUR_SERVER_IP/getchatts/.

You can retrace your steps to remove the certificate when you don’t need it anymore.

If you run into problem using HTTPS on your device, the error code displayed by Xcode may help you debug. This post has a list of them near the end of the thread.

Finally, change the serverUrl property of your ChattStore class from mada.eecs.umich.edu to YOUR_SERVER_IP. Build and run your app and you should now be able to connect your mobile front end to your back end via HTTPS.

Front-end submission guidelines

We will only grade files committed to the master or main branch (they are the same). If you use multiple branches, please merge them all to the master/main branch for submission.

Push your front-end code to the same GitHub repo you’ve submitted your back-end code:

:point_right: Go to the GitHub website to confirm that your front-end files have been uploaded to your GitHub repo under the folder chatter. Confirm that your repo has a folder structure outline similar to the following. If your folder structure is not as outlined, our script will not pick up your submission and, further, you may have problems getting started on latter labs. There could be other files or folders in your local folder not listed below, don’t delete them. As long as you have installed the course .gitignore as per the instructions in Preparing GitHub for EECS 441 Labs, only files needed for grading will be pushed to GitHub.

  441
    |-- chatter
        |-- swiftUIChatter
            |-- swiftUIChatter.xcodeproj
            |-- swiftUIChatter
    |-- chatterd
    |-- chatterd.crt

Verify that your Git repo is set up correctly: on your laptop, grab a new clone of your repo and build and run your submission to make sure that it works. You will get ZERO point if your lab doesn’t build, run, or open.

IMPORTANT: If you work in a team, put your team mate’s name and uniqname in your repo’s README.md (click the pencil icon at the upper right corner of the README.md box on your git repo) so that we’d know. Otherwise, we could mistakenly think that you were cheating and accidentally report you to the Honor Council, which would be a hassle to undo. You don’t need a README.md if you work by yourself.

Invite eecs441staff@umich.edu to your GitHub repo. Enter your uniqname (and that of your team mate’s) and the link to your GitHub repo on the Lab Links sheet. The request for teaming information is redundant by design.

References

General iOS and Swift

Getting Started with SwiftUI

SwiftUI at WWDC

SwiftUI Programming

State Management

Networking

Working with JSON


Prepared for EECS 441 by Ollie Elmgren, Tiberiu Vilcu, Nowrin Mohamed, and Sugih Jamin Last updated: December 10th, 2024