Tutorial: Chatter SwiftUI
Cover Page
DUE Wed, 09/10, 2 pm
This tutorial can be completed on the iOS simulator.
Preparing your GitHub repo
- On your laptop, navigate to
YOUR*TUTORIALS/
- Create a zip of your
llmprompt
folder - Rename your
llmprompt
folder**chatter**
- Check whether there’s a
DerivedData
folder in yourswiftUIChatter
folder; if so, delete it:laptop$ cd YOUR*TUTORIALS/chatter/swiftUIChatter laptop$ ls -d DerivedData # if DerivedData exists: laptop$ rm -rf DerivedData
- Push your local
YOUR*TUTORIALS/
repo to GitHub and make sure there’re no git issues:<summary>git push</summary>
- Open GitHub Desktop and click on
Current Repository
on the top left of the interface - Click on your assignment GitHub repo
- Add Summary to your changes and click
Commit to main
- If you have pushed other changes to your Git repo, click
Pull Origin
to synch up the clone on your laptop - Finally click on
Push Origin
to push changes to GitHub
- Open GitHub Desktop and click on
Go to the GitHub website to confirm that your folders follow this structure outline:
reactive
|-- chatter
|-- swiftUIChatter
|-- swiftUIChatter.xcodeproj
|-- swiftUIChatter
|-- chatterd
|-- chatterd.crt
|-- llmprompt.zip
If the folders in your GitHub repo does not have the above structure, we will not be able to grade your assignment and you will get a ZERO.
ChattStore
We now tackle networking for the app, but first add:
import Synchronization
to your ChattStore.swift
; then add the following properties near the top of your ChattStore
class, right below the chatts
array declaration:
private let nFields = Mirror(reflecting: Chatt()).children.count
private let mutex = Mutex(false)
private var isRetrieving = false
Mirror(reflecting: Chatt()).children.count
uses introspection to obtain the number of properties in the Chatt
type. We store the result in the variable nFields
for later validation use.
Now add the postChatt(_:errMsg:)
method inside the ChattStore
singleton:
func postChatt(_ chatt: Chatt, errMsg: Binding<String>) async {
let jsonObj = ["username": chatt.username, "message": chatt.message]
guard let requestBody = try? JSONSerialization.data(withJSONObject: jsonObj) else {
errMsg.wrappedValue = "postChatt: JSONSerialization error"
return
}
guard let apiUrl = URL(string: "\(serverUrl)/postchatt") else {
errMsg.wrappedValue = "postChatt: Bad URL"
return
}
var request = URLRequest(url: apiUrl)
request.httpMethod = "POST"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = requestBody
do {
let (_, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, http.statusCode != 200 {
errMsg.wrappedValue = "postChatt: \(http.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: http.statusCode))\n\(apiUrl)"
}
} catch {
errMsg.wrappedValue = "postChatt: POSTing failed \(error)"
}
}
We post a chatt
to chatterd
similar to how we posted an Ollama prompt in the
previous tutorial. Unlike the llmprompt
API, however, the postchatt
API is not expected to return anything on successful post.
Next add the getChatts(errMsg:)
method to the class:
func getChatts(errMsg: Binding<String>) async {
// only one outstanding retrieval
mutex.withLock { _ in
guard !self.isRetrieving else {
return
}
self.isRetrieving = true
}
defer { // ignore the warning about defer at end of scope
mutex.withLock { _ in
self.isRetrieving = false
}
}
// do the get
}
To prevent multiple outstanding getChatts(errMsg:)
, we let getChatts(errMsg:)
proceed only if there is no outstanding getChatts(errMsg:)
presently retrieving
from the backend. To ensure thread safety, we allow only mutually exclusive
access to the isRetrieving
variable. If there were no other outstanding
getChatts(errMsg:)
running, we proceed and then, before leaving the function,
we signal to other future getChatts(errMsg:)
that we’re done with this retrieval
so that they can perform their own retrieval. Replace // do the get
comment with:
guard let apiUrl = URL(string: "\(serverUrl)/getchatts") else {
errMsg.wrappedValue = "getChatts: Bad URL"
return
}
var request = URLRequest(url: apiUrl)
request.httpMethod = "GET"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Accept")
do {
let (data, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, http.statusCode != 200 {
errMsg.wrappedValue = "getChatts: \(http.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: http.statusCode))\n\(apiUrl)"
return
}
// unpack the data
} catch {
errMsg.wrappedValue = "getChatts: Failed GET request \(error)"
}
The above is again similar to how we received a response from the backend
in the llmprompt(_:errMsg:)
, though now we unpack the received data differently.
Replace // unpack the data
line with the following:
guard let chattsReceived = try? JSONSerialization.jsonObject(with: data) as? [[String?]] else {
errMsg.wrappedValue = "getChatts: failed JSON deserialization"
return
}
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]))
} else {
errMsg.wrappedValue = "getChatts: Received unexpected number of fields: \(chattEntry.count) instead of \(self.nFields)."
}
}
The server will return the chatt
s as a JSON Array. JSON (de)coding libraries such
as Swift Codable, which we used in the previous tutorial, work well with JSON objects
(“key”: “value” pair), but requires manually programming a recursive descent parser
to deserialize an array of unkeyed values, as used in the present tutorial, which
turns out to be more complicated than the manual decoding we have done here.
The UI
Now we update the app’s UI.
ChattViewModel
Edit the ChatViewModel
in file swiftUIChatterApp.swift
and update username
to
your uniqname (replace DUMMY_UNIQNAME
with your uniqname):
let username = "DUMMY_UNIQNAME"
SubmitButton
In ContentView.swift
, we update our SubmitButton
: search for the call to
llmPrompt(_:errMsg:)
and replace it with a call to postChat(_:errMsg)
,
which incidentally, does not need to have the timestamp
field of Chatt
to be
filled in:
await ChattStore.shared.postChatt(Chatt(username: vm.username, message: vm.message), errMsg: Bindable(vm).errMsg)
if vm.errMsg.isEmpty { await ChattStore.shared.getChatts(errMsg: Bindable(vm).errMsg) }
After the user successfully posted a chatt
, we automatically call getChatts(errMsg:)
to refresh our chatts
array (and have it displayed, with the display positioned at the last posted chatt
).
ContentView
To support human-to-human chat, we add two features to ContentView()
:
-
when the app starts, we automatically retrieve all existing
chatt
s and display them on the timeline, scrolled to the last postedchatt
, and -
since our backend doesn’t push
chatt
s posted by others, we allow user to performpull-to-refresh
to run anothergetChatts()
.
First add the following modifier after the call to ChattScrollView()
, below its
.onAppear { }
modifier:
.refreshable {
await ChattStore.shared.getChatts(errMsg: Bindable(vm).errMsg)
Task (priority: .userInitiated) {
withAnimation {
scrollProxy?.scrollTo(ChattStore.shared.chatts.last?.id, anchor: .bottom)
}
}
}
Note that refreshable { }
in this case doesn’t actually refresh the view, it calls getChatts(_:errMsg:)
which refreshes the chatts
array. SwiftUI then re-renders the view given the updated chatts
array, with the display positioned at the last posted chatt
. In this way, refreshable { }
refreshes our chatts
array with postings from other users.
To automatically retrieve chatt
s from the backend and scroll to the last posted
chatt
when ContentView()
appears, add the following modifier the VStack
in
ContentView()
, for example right after the .navigationBarTitleDisplayMode(.inline)
modifier:
.task (priority: .background) {
await ChattStore.shared.getChatts(errMsg: Bindable(vm).errMsg)
vm.showError = !vm.errMsg.isEmpty
Task (priority: .userInitiated) {
withAnimation {
scrollProxy?.scrollTo(ChattStore.shared.chatts.last?.id, anchor: .bottom)
}
}
}
Update the navigationTitle
to show “Chatter” as the app name.
Congratulations! You’re done with the front end! (Don’t forget to work on the backend!)
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
, by changing the serverUrl
property in your ChattStore
to mada.eecs.umich.edu
. Once you have your backend setup, change serverUrl
back to YOUR_SERVER_IP
. You will not get full credit if your front end is not set up to work with your backend!
Front-end submission guidelines
We will only grade files committed to the main
branch. If you use multiple branches, please merge them all to the 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 tutorial
- Add Summary to your changes and click
Commit to main
at the bottom of the left pane - If you have pushed code to your repo, 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 tutorials. 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 Reactive Tutorials, only files needed for grading will be pushed to GitHub.
reactive
|-- chatter
|-- swiftUIChatter
|-- swiftUIChatter.xcodeproj
|-- swiftUIChatter
|-- chatterd
|-- chatterd.crt
|-- llmprompt.zip
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 tutorial 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.
Review your information on the Tutorial and Project Links sheet. If you’ve changed your teaming arrangement from previous tutorial’s, please update your entry. If you’re using a different GitHub repo from previous tutorial’s, invite eecsreactive@umich.edu
to your new GitHub repo and update your entry.
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 by Ollie Elmgren, Tiberiu Vilcu, Nowrin Mohamed, and Sugih Jamin | Last updated: August 25th, 2025 |