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.
- Click
Create a new Xcode project
in “Welcome to Xcode” screen (screenshot) - Select
iOS > App
and clickNext
(screenshot) - Enter
Product Name
: swiftUIChatter -
Team
: Noneif you don’t have one yet, otherwise choose your
Personal Team
-
Organization Identifier
: edu.umich.<YOUR UNIQNAME>
👈👈👈replace
<YOUR UNIQNAME>
with yours, remove the angle brackets,< >
-
Interface
: SwiftUI -
Language
: Swift - Leave the boxes unchecked, click
Next
- On the file dialog box that pops up, put your
swiftUIChatter
folder in👉👉👉 YOUR*LABS*FOLDER/chatter/swiftUIChatter/
, whereYOUR*LABS*FOLDER
is the name you give to your 441 GitHub repo clone folder above. - Leave
Create Git repository on my Mac
UNCHECKED (screenshot). We will add the files to GitHub using GitHub Desktop instead. And leaveAdd to
to the default, “Don’t add to any project or workspace”. - 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
- Click on
Current Repository
on the top left of the interface - Click on the 441 GitHub repo you cloned above
- Add Summary to your changes and click
Commit to master
(orCommit to main
) at the bottom of the left pane - If you have a team mate and they have pushed changes to GitHub, you’ll have to click
Pull Origin
and resolve any conflicts, re-commit to master/main, and - Finally click on
Push Origin
to push changes to GitHub
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.
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:
-
swiftUIChatterApp
: named after your project, this file tells iOS the entry point (@main
) of your app. Only one data type (struct
) can be so tagged. This struct describes theScene
in which the window hierarchy of your app resides.WindowGroup
is the window hierarchy for yourScene
(we’ll discuss the keywordsome
later). Unlike on the iPads or Macs where an app can have multiple scenes, each app has only one scene on the iPhones. -
ContentView
: click on the name of this file and rename it fromContentView.swift
toMainView.swift
. It will hold ourchatt
s timeline later. Inside the file, search for the three occurences ofContentView
and replace each withMainView
.
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:
- Right click on the
swiftUIChatter
folder on the left/navigator pane - Select
New File...
- Choose an
iOS > Swift File
template and clickNext
. - For
Save As:
enter the nameChatt
and clickCreate
. - Replace the
import Foundation
line in the file with the following struct definition forChatt
:
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 chatt
s 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 chatt
s. Since the chatt
s 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 chatt
s 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 chatt
s 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 chatt
s 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 SwiftUIPreview
feature useful, you can choose to create aSwiftUI
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 chatt
s from the back end. If not, we can proceed. Checking and updating isRetrieving
is serialized using the synchronized
dispatch queue.
To retrieve chatt
s, 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 chatt
s 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 chatt
s from the back-end server, we return to MainView
to retrieve the chatt
s 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 chatt
s.
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, theprojectedValue
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>
Navigation between views
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 chatt
s from the back end, including chatt
s 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 chatt
s 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 chatt
s 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 chatt
s 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!
- If you AirDrop your
chatterd.crt
, skip to next step. If you emailed the certificate to yourself, view your email and tap the attachedchatterd.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 theFiles
app on your phone and in theDownloads
folder locate yourchatterd.crt
and tap it. - You should see a
Profile Downloaded
dialog box pops up. - Go to
Settings > General > VPN & Device Management
and tap on the profile withYOURSERVERIP
. - At the upper right corner of the screen, tap
Install
. - Enter your passcode.
- Tap
Install
at the upper right corner of the screen again. - And tap the somewhat dimmed out
Install
button. - Tap
Done
on the upper right corner of screen. -
Go back to
Settings > General
-
Go to
[Settings > General >] About > Certificate Trust Settings
-
Bravely slide the toggle button next to
YOURSERVERIP
to enable full trust of your CA’s certificate and clickContinue
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:
- Open GitHub Desktop and click on
Current Repository
on the top left of the interface - Click on the GitHub repo you created at the start of this lab
- Add Summary to your changes and click
Commit to master
(orCommit to main
) at the bottom of the left pane - Since you have pushed your back end code, you’ll have to click
Pull Origin
to synch up the repo on your laptop - Finally click
Push Origin
to push all changes to GitHub
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
- Apple Developer web site
- Swift guide
- Apple developer enrollment
- TestFlight
- Submitting app to App Store
Getting Started with SwiftUI
- WWDC20: Advancements in SwiftUI
- Rethink iOS Programming with SwiftUI and Combine
- Quick guide on SwiftUI essentials
- A guide to the SwiftUI layout system - Part 1
- How to effectively leverage the power of new #Preview feature in SwiftUI
- Making SwiftUI Previews Work for You (old version, may still be useful?)
- SwiftUI Previews (old version, may still be useful?)
SwiftUI at WWDC
- Introducing SwiftUI: Building Your First App
- Introduction to SwiftUI
- SwiftUI Essentials
- App Essentials in SwiftUI
- Data Flow Through SwiftUI
- Data Essentials in SwiftUI
- Stacks, Grids, and Outlines in SwiftUI
- Integrating SwiftUI
SwiftUI Programming
- The New Navigation System in SwiftUI
- Custom navigation bar title view in SwiftUI
- How to add button to navigation bar in SwiftUI
- The future of SwiftUI navigation (?)
- How to Deal With Modal Views in SwiftUI
- How to get row index in SwiftUI List?
- How do I modify the background color of a List in SwiftUI?
- How to convert
UIColor
to SwiftUI’sColor
State Management
- State and Data Flow
- The @State Property Wrapper in SwiftUI Explained
- Discover Observation in SwiftUI
- A Deep Dive into Observation
- Working with @Binding in SwiftUI
- Stanger things around SwiftUI’s state
- The Inner Workings of State Properties in SwiftUI
- Observer vs Pub-Sub pattern
Networking
Working with JSON
-
Swift Tip: String to Data and Back for use in
getChatts()
-
Convert array to JSON in swift for use in
postChatt(_:)
- How can I define Content-type in Swift using NSURLSession
- How to parse JSON using Coding Keys in iOS
Prepared for EECS 441 by Ollie Elmgren, Tiberiu Vilcu, Nowrin Mohamed, and Sugih Jamin | Last updated: December 10th, 2024 |