Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
iOS Development is a specialized version of mobile application development, pertaining specifically to iOS devices. iOS refers to the mobile operating system created by Apple and is what powers many of the company's devices, including the iPhone, iPad, MacOS, and iPod Touch.
iOS development includes the construction of the user interface of an app, the handling of user interactions across the app, and the management of user data throughout the app. There are a number of ways to build iOS applications but the primary one is using Swift and in the Xcode IDE. The two native primary frameworks for building iOS applications are UIKit and SwiftUI.
With iOS capturing almost 60% of the mobile operating system market share in the United States, iOS devices have become a ubiquitous part of daily life. Every day, you use your phone to communicate with friends, navigate to places, and keep up with social media. iOS development sits at the heart of all of the apps that you use for these activities. By taking this course, you will learn how to build an application from the ground up and gain the skills to transform any application idea into a working product.
Moreover, the skills that you learn in this course are broadly applicable to understanding other front end frameworks as well. As a result, many students who have completed this course end up more prepared to recruit for internships and build out their own products.
In this course, you will learn all the necessary components that comprise an iOS application. We begin with an introduction to Swift, the primary programming language for iOS. Then we will move on to teaching user interface development in UIKit, showing you how to build beautiful interfaces across all iOS mobile devices and how to visually organize data in applications.
Afterwards, we will proceed with lectures on networking, teaching you how to integrate your application with backend services by pulling data from and saving data to backend services.
From then on, we will delve in SwiftUI, a completely different (but equally useful) framework for building UI. Finally, we will learn advanced functionality such as creating push notifications, setting up authentication system. To put everything together, students will work on the Hack Challenge, a hackathon in which students from all AppDev courses build a full stack application from scratch. The winners will have their app features on the AppDev website!
Fall 2025
Here are some pointers for gearing up for our first class on October 15th, 2025!
Ensure you've read through the rest of this page and filled out all necessary forms.
Install XCode from the App Store! (P.S. it takes a while 😄)
Join us on Ed, send us questions via email @[email protected], and get hyped!
CS 1110 is a highly recommended co/prerequisite, but not required. You will also need access to a MacBook (Intel - macOS Catalina 10.15.4+, Apple Silicon - macOS Big Sur 11+) to participate in the course (the Xcode IDE is only available for macOS).
Enroll in CS 1998-601 in Student Center. This is a 2 credit S/U course, but you may enroll for 1 credit to avoid going over the credit limit.
We will be using Ed Discussion for class communication and answering questions. Ed will be the main method of communication between students and course staff. You can join the Ed .
Swift 5, Xcode 12.0+, and iOS 13.0+ is required!
You can install Xcode through the .
If you do not have the latest Mac version and cannot update, you can find older Xcode versions .
We will be using . Double check that you can log in.
We will be using for app designs. You can create an account using your Cornell email.
Spring 2024 | Vin Bui
Coming soon!
Spring 2024 | Vin Bui
Coming soon!
git clone https://github.com/intro-to-ios/lec5-uicollectionview.git
OR git clone [email protected]:intro-to-ios/lec5-uicollectionview.gitgit checkout origin/1-collectionview
OR git checkout 1-collectionviewgit clone https://github.com/intro-to-ios/lec3-navigation.git
OR git clone [email protected]:intro-to-ios/lec3-navigation.gitgit checkout origin/1-navigation
OR git checkout 1-navigation
git checkout origin/2-delegation
OR git checkout 2-delegationgit clone https://github.com/intro-to-ios/lec4-uitableview.git
OR git clone [email protected]:intro-to-ios/lec4-uitableview.gitgit checkout origin/1-cell
OR git checkout 1-cell
git checkout origin/2-tableview
OR git checkout 2-tableviewFall 2025
Monday
Check out for all building codes on campus!
Fall 2023 | Vin Bui
In almost any program that we create, we will need to store data at some point. In Swift, we can store data in two ways: variables and constants. We can think of both variables and constants as a box holding some value inside. However, there is one key difference between these two. A variable can change its value whenever we want. On the contrary, a constant can hold a value once and can never be changed again.
It may seem pointless to have both variables and constants; however, there are many advantages. If Xcode knows that a value will never change, it will optimize our program to make it run faster. Another advantage is that if we were to make a mistake and change a value of a constant when we don’t need to, Xcode will tell us and our code will not compile.
To create a variable, we use the var keyword.
To change the value of the variable, we can simply do the following.
Let’s try this in the Xcode playground.
Notice how we do not need to use the var keyword the second time. We should only use the var keyword if we are declaring a new variable. We can test this out in the Xcode playground.
Now, what if we wanted to use a constant instead of a variable? All we would need to do is to use the let keyword instead.
As we can see, changing the instructor variable to a constant caused Xcode to get angry. The error message clearly informs us that we are attempting to change the value of a constant.
Fall 2023 | Richie Sun
UIKit provides a variety of features for building apps, including components we can use to construct the core structure of our iOS apps. The framework provides the core objects that we need to build apps for iOS. We use these objects to display our content onscreen, to interact with that content, and to manage interactions with the system. Apps rely on UIKit for their basic behavior, and UIKit provides many ways for us to customize that behavior to match our specific needs.
The UIKit framework provides an extensive library of classes that represent different kinds of views that we can utilize in our mobile apps. Thus, in order to create and customize these views, we need a basic understanding of classes, properties, methods, and inheritance. For example, suppose we wanted to display a title with a single line of text, we would first call upon the constructor to create an instance of UILabel:
Every view has a different set of properties that we can utilize to customize our frontend view. In the case of the UILabel, we can define many elements of the text such as, text, font, textColor, textAlignment, etc.
Thus, we are able to change the properties of the UILabel to display a customized view for our iOS App.
Before we write any code, let's make sure to properly set up our Swift files as detailed below. In this class we mainly teach UIKit with programmatic layout, and we DO NOT use Storyboard Swift. Follow the guide here:
Fall 2023 | Vin Bui
In the previous section, we mentioned how data that is sent across the internet is represented as JSON. In this section, we will discuss how to decode JSON data and use it in our Swift code.
In Swift, there are two protocols that our model structs/classes must conform to in order to be decoded: Decodable and Encodable. However, we commonly use the protocol Codable instead which essentially means that we are conforming to both protocols.
These protocols are very powerful and allow us to customize our own JSON decoder and encoder. For the scope of this course, we will only touch the bare surface.
Consider the following JSON data representing a student:
This is the information that we are given from the backend that represents a student. As a frontend developer, we need to make sure that both the frontend and the backend agree on one schema. Given this data, we have to decide on how to create our data model objects.
There are five keys in this JSON here: age, classes, first_name, last_name and major. Normally, we want the name of our properties to match exactly with the keys in the JSON (although there is a way to customize this). Once we have the names down, we need to determine the type of each property. Notice that the value for age does not have quotes around it so it is not a String. Also notice that the value for classes contains square brackets, indicating that it is an array where each element is a String. This gives us the following Student model:
Notice how the JSON has the key first_name and last_name, but in the model we use firstName and lastName. Although JSON is typically represented with camelCase, there are times when it may use snake_case (for example in our course). Since Swift's convention is to use camelCase, it has a built-in decoder/encoder that provides a neat way to deal with this, which we will look at in the next section.
Fall 2023 | Reade Plunkett
Widgets enable you to view timely information from your favorite apps at glance in various environments across all Apple platforms. Introduced in iOS 14, widgets have become increasingly more flexible, adaptable, and powerful throughout the past few years. You may have experience using widgets on your iPhone’s home screen, but widgets can be used in many other areas on your devices.
Home Screen: The most common widget is one that lives on your home screen, such as the Apple Weather, News, or Reminders widgets on iOS and iPadOS
Today View: If you swipe right on your home screen, you can also view various widgets in your Today View on iOS and iPadOS.
Lock Screen: On both iOS and iPadOS, widgets can now be added to your lock screen for quick viewing when you turn on your phone.
This textbook was originally created in Fall 2023 by Vin Bui. Over the course of time, more chapters have been added by other instructors (see contributors below). Since the content may become outdated, all chapters will contain date information of when it was written. The lecture videos are from Fall 2023, but feel free to check out other semesters on the Cornell AppDev YouTube Channel.
Peter Bidoshi - Instructor FA24, iOS Lead FA25
Richie Sun - Instructor FA23/SP24, iOS Lead FA24
Vin Bui - Instructor FA23, iOS Lead SP24
Tiffany Pan - Instructor SP24, iOS Lead FA23
Fall 2025
Pre-Class TODOs
Fall 2023 | Vin Bui
Of course our app won’t have just a single screen — we typically have multiple screens that we want to navigate to and from. In SwiftUI, navigation between views is very simple and similar to that of UIKit.
For iOS 16 and later, we use a NavigationStack which is a view that displays a root view and enables us to present additional views over the root view. This is similar to a UINavigationController in UIKit.
git clone https://github.com/JiwonJeong414/BetterRest.git
OR git clone [email protected]:JiwonJeong414/BetterRest.gitReade Plunkett - iOS Lead FA22/SP23
3:30-4:30 pm
KMB B11 (Kimberly Hall)
Zain, Jiwon
Tuesday
12:00-1:00 pm
UPS 216 (Upson Hall)
Jay, Angie
Wednesday
3:00-4:00 pm
UPS 102 (Upson Hall)
Asen, Arielle
Thursday
4:30-5:30 pm
KND 101 (Kennedy Hall)
Caitlyn, Charles
Friday
12:00-1:00 pm
STL 198 (Statler Hall)
Andrew, Adelynn
Desktop: Widgets can now be added to your Mac desktop with MacOS Sonoma.
Notification Center: On Mac, you can also view widgets in your Notification Center.
Apple Watch: Widgets also appear on your Apple Watch, although they are typically referred to as “complications” in this context.


Fall 2023 | Vin Bui
SwiftUI is a declarative framework that allows us to create the user interface in our apps. It was introduced back in 2019 as a replacement for UIKit. The best way to understand declarative UI is to compare it to imperative UI.
If you are familiar with languages such as Java or C++, these are imperative languages. Imperative programming defines how tasks should be accomplished. For example, say we plan on cooking steak for dinner. The imperative approach would be:
Walk to the meat section in the grocery store.
Search for any piece of fine steak starting from the top shelf to the bottom.
Once found, check the price and weight.
Place it in our shopping cart if we are satisfied.
As we can see, these steps tell us how we should shop for a piece of steak once we enter the grocery store. Now, compare this to the declarative approach:
Search for any satisfactory piece of steak in the grocery store.
There is one single step and that step tells us what to do. In other words, declarative programming defines what tasks should be accomplished compared to **how. When we think about computation, the imperative paradigm defines the control flow and state changes whereas the declarative paradigm defines only the logic.
If you are familiar with creating an app using the UIKit framework, you can see why UIKit is an imperative approach to creating UI. If we have a UILabel, then to change the state of that label, we could modify the properties of that object (such as .text). State is just another way of saying “the values that we store in our code.” The problem with imperative UI is that we need to constantly keep track of the state of our code and ensure that our UI correctly reflects that state.
For example, we could have a UIButton that, when tapped, triggers a change in the state of a UILabel. If we were to introduce another UIButton or an additional UILabel, we would need to write code to ensure that all of these UI components properly reflect the current state. As we add more components to our app, we can see that things can get pretty complicated.
With SwiftUI and its declarative approach, we can create a Text view that is associated with some state variable. When the value of this variable changes (in other words, there is a state change), all views that rely on that state is updated accordingly. Everything stays in sync and is handled automatically. We no longer have to update our views manually when data changes.
That is the beauty of SwiftUI. We told it what to show based on the current state and SwiftUI moves between user interface layouts for us. That is the declarative approach. We define rules that our views should follow and SwiftUI enforces those rules.
SwiftUI is relatively new and is only available for iOS 13 and above. It is also a growing framework and there are still a lot of improvements that need to be made. If you are new to iOS development and are deciding to choose between SwiftUI and UIKit, then I recommend learning SwiftUI. However, although SwiftUI is the future, many apps are currently built in UIKit. UIKit will still be needed for a few more years, so it is important to know both.
Spring 2024 | Vin Bui
Coming soon!
This chapter only covers Xcode Cloud, but there are other CI/CD services that are commonly used such as Codemagic, fastlane, Jenkins, and GitHub Actions.
1️⃣Xcode Cloud2️⃣AppStore Shipping


let titleLabel = UILabel(){
"age": 19,
"classes": [
"CS 1998",
"PHYS 2213"
],
"first_name": "Vin",
"last_name": "Bui",
"major": "Info Sci"
}Post some data to an endpoint
Download an image from a URL
Authentication with a REST API
A3 and A4 starter code should already contain Alamofire via Swift Package Manager. Alternatively, we can install Alamofire with CocoaPods:
Simply add the line pod 'Alamofire' to our Podfile like so:
Note that your Podfile will look different depending on the name of your project. In this case, my project is called “MyApp”. Also, make sure to open the Xcode workspace and not the project.
# Uncomment the next line to define a global platform for our project
# platform :ios, '9.0'
target 'MyApp' do
use_frameworks!
# Pods for MyApp
pod 'Alamofire'
endNavigationViewTo modify the navigation, we can use modifiers. However, the modifier must be placed in the view nested inside of the NavigationStack, not on the NavigationStack itself.
For example, if we wanted to add a title to the navigation bar, we can use the .navigationTitle modifier. Note that we are using the modifier on the Text view which is inside of the NavigationStack.
To push a view, we use a NavigationLink, which is a view that controls a navigation presentation. This is similar to pushing a ViewController in UIKit. There are many different ways to use this; however, the preferred and conventional way to use it is with the trailing closure syntax:
NavigationStack {
// Place views here
}var instructor = "Vin"var instructor = "Vin"
instructor = "Richie"titleLabel.text = "UIKit is Awesome!"
titleLabel.font = UIFont(named: "Comic-Sans", size: 16)
titleLabel.textColor = UIColor.red
titleLabel.textAlignment = .centerstruct Student: Codable {
let age: Int
let classes: [String]
let firstName: String
let lastName: String
let major: String
}NavigationStack {
Text("AppDev")
.navigationTitle("Some Title Here")
}NavigationStack {
NavigationLink {
// View that is being pushed (the destination)
} label: {
// View that is being tapped on (such as a Text)
}
}Join our Ed Discussion. This is how we know you are taking the course! We will only add people to the Github and CMSX who join the Ed Discussion.
Enroll on Student Center (CS1998-601) if you want credit for this course.
git clone https://github.com/intro-to-ios/lec2-uikit
OR git clone [email protected]:intro-to-ios/lec2-uikit.git### Part II ###
git clone https://github.com/AsenKimO/demo-mvvm.git
OR git clone [email protected]:AsenKimO/demo-mvvm.git### Part I ###
git clone https://github.com/intro-to-ios/lec10-swiftui-1.git
OR git clone [email protected]:intro-to-ios/lec10-swiftui-1.git### Part I ###
git checkout origin/1-layouts
OR git checkout 1-layouts
git checkout origin/2-list
OR git checkout 2-list
git checkout origin/3-refactor
OR git checkout 3-refactorgit clone https://github.com/intro-to-ios/lec7-networking2.git
OR git clone [email protected]:intro-to-ios/lec7-networking2.gitgit checkout origin/1-get
OR git checkout 1-get
git checkout origin/2-post
OR git checkout 2-post
git checkout origin/3-refresh
OR git checkout 3-refreshgit clone https://github.com/intro-to-ios/lec8-persist-snapkit.git
OR git clone [email protected]:intro-to-ios/lec8-persist-snapkit.gitgit checkout origin/1-userdefaults
OR git checkout 1-userdefaults
git checkout origin/2-snapkit
OR git checkout 2-snapkitgit clone https://github.com/intro-to-ios/lec3-navigation.git
OR git clone [email protected]:intro-to-ios/lec3-navigation.gitgit checkout origin/1-navigation
OR git checkout 1-navigation
git checkout origin/2-delegation
OR git checkout 2-delegationgit clone https://github.com/intro-to-ios/lec7-networking2.git
OR git clone [email protected]:intro-to-ios/lec7-networking2.gitgit checkout origin/1-get
OR git checkout 1-get
git checkout origin/2-post
OR git checkout 2-post
git checkout origin/3-refresh
OR git checkout 3-refreshgit clone https://github.com/intro-to-ios/lec8-persist-snapkit.git
OR git clone [email protected]:intro-to-ios/lec8-persist-snapkit.gitgit checkout origin/1-userdefaults
OR git checkout 1-userdefaults
git checkout origin/2-snapkit
OR git checkout 2-snapkitgit clone https://github.com/intro-to-ios/lec5-uicollectionview.git
OR git clone [email protected]:intro-to-ios/lec5-uicollectionview.gitgit checkout origin/1-collectionview
OR git checkout 1-collectionviewOriginal Author: Vin Bui
The Hack Challenge is an AppDev courses tradition where students across our 4 courses (iOS, Android, Backend, and DPD) come together to create their own mobile app in 2 weeks.
The purpose of our courses is to help our students gain skills that they can take into industry. The best way to develop these skills is by pursuing projects, especially with a team. Additionally, you will be able to put this on your portfolio, which will be an important factor when applying for internships.
This depends on the number of students we have across all courses. However, most teams typically consist of 2-3 frontend members, 1-2 backend members and 1 designer. We will do a team matching mixer when the Hack Challenge begins. During this mixer, you will meet students in other courses and form a team.
Each team will also have a frontend mentor as well as a backend mentor to provide any help if needed.
As a reminder, the Hack Challenge is worth 30% of your final grade. For iOS, you are required to have the following:
Multiple screens that you can navigate between OR at least one scrollable view.
Final Submission
Multiple screens that you can navigate between.
At least one scrollable view.
Networking integration with a backend API.
Note that you can use either UIKit or SwiftUI for the Hack Challenge.
Yes! We have prizes and awards for the following:
🏆 Best Overall
💻 Best Backend
📱 Best UI
🎨 Most Creative
Fall 2023 | Vin Bui
You can access the Cornell GitHub enterprise here.
To check to see if you have git installed, open Terminal and run git --version. A message will display if you already have it installed.
Follow the instructions to install Git. I personally recommend using the Homebrew approach (requires you to install ), but the Binary Installer should work as well.
Log in to your GitHub. Go to Settings > SSH and GPG Keys. Create a New SSH key.
Keep the page open. Open up Terminal and execute the two commands (replace YOUR NAME and YOUR GITHUB EMAIL):
git config --global user.name "YOUR NAME"
git config --global user.email "YOUR GITHUB EMAIL"
For newer Macs (12, Monterey/Ventura), run
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
If that command fails, try ssh-add --apple-use-keychain ~/.ssh/id_rsa
For older Macs (11, Big Sur), run ssh-add -K ~/.ssh/id_ed2551
If that command fails, try ssh-add -K ~/.ssh/id_rsa
Open up Terminal and change the directory to wherever you want the repository files to be located. You can change the directory using the cd command.
For example, if I want the repository files to be located in my Desktop, I would type cd Desktop
To clone a repository, simply type git clone <ENTER URL HERE>
Spring 2024 | Vin Bui
In the course, we only utilized some basic functionalities of Git such as staging, committing, and pushing. However, Git is a lot more powerful than that which we will discuss in this chapter.
In the course, we mainly focused on one working branch known as main or master. When we made commits, we typically pushed them all into this one branch. When using this approach, collaborating with others was very difficult to do. If we did not fetch and pull from the main branch before working, we often had to deal with merge conflicts. Now, imagine if both developers are working on the same branch concurrently — a merge conflict is very likely to occur.
To avoid having to deal with merge conflicts every time we push to a branch, it’s often better to create separate branches when collaborating. To create a new branch and switch to it, use the following command:
Pull requests (also known as PRs) allows us to discuss changes that were pushed onto a branch before merging it to the base branch. Let’s look at the following scenario.
Suppose we have a branch called A whose base branch was from main. We push commits and make changes to the branch A like how we would if it was the main branch. When we are ready to push these changes to the main branch, we can submit a pull request through GitHub to merge A to main. This allows other developers to review our code before merging it to the main branch. If changes need to be made, then we can push new commits to our A branch and request a review again.
When should we create a PR? There is no right or wrong answer to this question. A PR can be a single line of code or even a thousand lines. A good rule of thumb is to create a PR if it’s important to the state
A huge benefit of creating branches and making PRs is that we can chain them together. Say we have two features, A and B, that need to be implemented. We create a branch off of main and call it A. When we are done with making changes to A, we can create a PR to merge it to main.
While the PR is being reviewed, we may need to work on a new feature (in this case feature B). What we can do is create a branch B from A (not main). Now, if we create a PR, we want to create a PR to merge B to A instead of main. This way, we do not have to wait for the previous PR to be reviewed. Additionally, only the changes made between A and B will be shown in the PR, making it a lot easier to review.
When both PRs are approved, we want to merge in the following order:
Merge the PR from A to main.
Delete the A branch. The target branch in the second PR will automatically change once A gets deleted.
Merge the PR from B
There are times when we may want to merge a working branch into another without creating a PR. For example, if we are on a branch A, we can simply merge main into our branch with the following command:
When we run this command, we may receive merge conflicts. In other words, there are conflicts between the changes of the two branches, preventing the merge from happening. My preferred way to resolve merge conflicts is by using GitHub Desktop with Visual Studio Code (VSCode). VSCode will tell us where the conflict is occurring and allow us to resolve it quickly by clicking on “Accept Current Change” or “Accept Incoming Change”. See image below.
Of course, we can use our code editor to resolve the merge conflicts, but it can be difficult to locate where exactly the conflict is. GitHub Desktop works seamlessly with VSCode, allowing us to easily pinpoint and resolve the conflicts.
There are times when we may want to undo a commit, such as when we push sensitive information to a public repository. To undo a commit, we can simply use the following command
where 2 means move backwards by 2 commits (we can replace this with a number of our choice). Note that this is a soft reset meaning that staged files are not reverted back to a previous state. In other words, we keep the code that we wrote on our local repository.
We can then create a new commit and then force push our changes to the repository:
Fall 2024 | Peter Bidoshi
Now that we have learned a bit about functions and how they work, we will discuss something more general, called Closures.
Closures are self-contained blocks of functionality that can be passed around and utilized within your code. The most simple example of closure is a function, but there exist others that are very important to understand before exploring future topics.
Imagine we have a function, which is a type of closure:
func getData() -> String {
return "Fetched Data"
}One way we could use the return of this function is as follows:
// Same code from above
func getData() -> String {
return "Fetched Data"
}
// Save the string returned from the function in a variable
var data = getData()
// Print the variable
print(data)Pretty simple right? However, we could also achieve this differently by using a Closure Expression. Closure expressions allow us to pass a piece of code as an argument to a function. To incorporate this, we need to change the method signature of our "getData" function.
// This is the syntax to indicate that this
// function takes in another function!
// "handler" is just the name of the variable,
// and we can call it whatever we want
func getData(handler: (String) -> Void) {
handler("Fetched Data")
}What is going on here? Let's first talk about the method signature. You will notice we added an argument to the function that is called "handler". This argument has a type of "(String) -> Void". This represents a function that needs to take in a String and return nothing (void). The "getData" function then passes the String "Fetched Data" into that function. Now, let's change the other part of our code.
Here, we created a function called "printData" that takes a string and returns nothing (void). Notice that this signature (string -> void) is the same signature as our "handler" argument. We then pass the "printData" function as an argument to our "getData" function. with this, our "getData" function will pass the String "Fetched Data" to the "printData" function that was passed to it. This will achieve the same result as the original code.
However, this can be simplified! Instead of writing out a whole other function, let's provide the code right into the function! This is what a Closure Expression is all about.
Nice! "data in" is a tricky syntax that you will have to remember. The "data" represents a String that will be returned to the handler after the getData function is run. Swift is smart, so it can automatically infer the type of "data" as a String due to the getData method signature.
Fall 2023 | Vin Bui
In the previous chapter, we learned what views are and how to create views in UIKit. We also learned how to use AutoLayout to position our views on the screen. However, our views contained hard coded data and did not have any functionality at all. When we create apps, we want our views to respond and update to user interactions.
MVC (Model-View-Controller) is a software design pattern which is a set of rules that govern the architecture that we follow when writing our code. There are other design patterns out there such as MVVM (Model-View-ViewModel) but we will be using MVC throughout this course. To better understand MVC, let’s take a deeper look at each component.
Models are objects that represent our application’s data. Let’s look at an app we are familiar with, Eatery. The models in Eatery are the dining halls, the dishes and food items, the user’s information, etc. In other words, models contain information about our application and are often updated based on user interaction or from a backend service.
Notice that we often use structs instead of classes when representing our models. The difference between these two is that structs are value types whereas classes are reference types. What this means is that if we were to change a property of an instance of a struct, the entire instance changes. On the other hand, if we change a property of an instance of a class, the instance does not change.
For example, consider a User model with the property name. For a class object (reference type), if we changed the value of name, all locations that have a reference to that object have the new updated value for name. If this were to be a struct object (value type), changing the value of name in one location DOES NOT update other locations because a new instance is being created. We can think of reference types as sharing a Google Doc with other collaborators. If one person were to change something on their side, the others would be able to see those changes. For structs, we can think of it as making a copy of the Google Doc.
One advantage of using a class is inheritance. In other words, we can use properties and methods already defined by a parent class, commonly seen in UIKit. However, there are times when we don't need to use all of these properties and methods, making structs more preferable. This is commonly seen in SwiftUI and why using structs is a lot faster than classes.
Views are the visible components that are used to build the user interface (UI). This includes buttons, labels, images, etc. In Eatery, everything we see is a view. For example, the name of the dining hall is a UILabel which contains information from the dining hall model. We don’t see models. We see views that contain information from the models.
Controllers belong in the middle between models and views. The main goal of a controller is to establish a connection between models and views. The controller is also in charge of processing the logic to update the models and views. For example, in Eatery, there is a User model that represents the logged in user. When we sign in, we are interacting with views. How does the model update based on what we passed into the text field? Well the controller handles that logic. If we provided the correct credentials, it will modify the models and update the views such as showing a logout button. If we provided invalid credentials, then the controller will still update the view and show a red error message.
One important thing to keep note of is that views and models are separate. They cannot directly interact with each other, but rather, they must go through a controller to make that connection.
We can think of MVC as watching television: the TV is the view, the remote is the controller, and the channels/content is the model. How does the TV (view) change the channel (model)? It can’t! We must use a remote (controller) in order to do that. When we change channels using the remote, we made changes to the model and tell the TV to update its view.
Fall 2023 | Vin Bui
So far, we only created static, non-moving views. However, many apps today have views that are scrollable. One way to implement a scrollable view with UIKit is with a UITableView.
A UITableView is a subclass of UIScrollView which is a view that users can scroll through. We can think of a UITableView as a list of data. Each item inside of this list is represented by a view called a UITableViewCell. Each cell tends to look very similar to one another but are holding different data.
A UITableView contains sections where each section contains rows, and each row being represented by a cell. Let’s take a look at Settings:
As we can see, a UITableView can contain as many sections as it wants, and each section can contain as many rows as it wants. The rows are represented by a UITableViewCell which is a view. In the image above, we can see that each cell looks very similar to one another. Each cell contains a UIImageView representing the icon as well as a UILabel describing that cell. In the next section of this chapter, we’ll learn how to create these custom cells.
Fall 2023 | Richie Sun
For more information, check out the Git Documentation
Pulls changes from a remote repository into the current branch. If the current branch is behind the remote, then by default it will fast-forward the current branch to match the remote.
Displays paths that have changes between your local repository and the HEAD of the remote repository
This command updates the index using the current content found in the working tree, to prepare the content staged for the next commit. To stage all changes, use git add .
Create a new commit containing the current contents of the index and the given log message describing the changes.
Updates the main branch of the remote repository based on the last local commit
Lists all existing branches; the current branch will be highlighted in green and marked with an asterisk
Fall 2023 | Vin Bui
We learned that we can use an HTTP request to retrieve information from a server. We also learned that the server responds with a status code and some data represented by JSON. Now, how do we make these API calls in Swift?
Before we look at Swift code, let’s try to understand how a networking call works in the perspective of the client. Grab a phone and open Instagram or any social media app. When we launch the app, not all of the information is fetched from the backend right away. If we had a slower internet connection, these posts and stories will take even longer to load. Now notice how the app does not freeze even though we are sending network requests to the backend server. What’s really happening is that these network calls are happening asynchronously. In other words, it is running in the background.
Why is this necessary? A client’s internet connection can vary and we do not know exactly how long it will take to receive a response back from the server. Because of this, we do not want our app to freeze while we are fetching information from the backend. Additionally, we want to be notified as soon as the server sends a response back so that we can perform any UI updates. In the case of Instagram, once the client receives these posts on their feed from the server, the client is notified and the app automatically updates the UI. There are many ways we can handle asynchronous code in Swift. In this course, we will be using a traditional approach using callbacks (completion handlers).
A callback is a function that is passed into another function as an argument and is called after the original function completes. In Swift, these callback functions are known as completion handlers. Imaging this scenario:
You are expecting a package delivered to your house containing a gift for your parents. Unfortunately, you are out of town and will not be able to deliver the gift yourself. You ask your friend that lives in your house to give it to your parents once the package arrives. Your friend could do two things:
Stand at the front door and wait for the package for days without doing anything else, then deliver it to your parents
Have common sense and go on with life while they wait for the package to arrive, and then deliver it once it arrives
In this example, the package delivery is the network request. Your friend delivering it to your parents once the package arrives is the callback. You are the original function that requested this callback function to do something. Another common real-life example would be asking someone to call you back once they are ready to call you back instead of staying on the line.
Consider a social media app similar to Instagram. We have a Post object representing a post. When we open our feed page, we send a request to our backend server to fetch new posts. Our function header to fetch our feed could look something like this:
We create a function called fetchFeed that takes in another function as an argument (the callback) with the name completion. We can name this callback function to whatever we want, but make sure it is clear that it is a completion handler. So this completion parameter needs to have a type, as with any parameter inside of a function header. Well, we want this parameter to be a function type in order for it to be a callback. When we declare a function type, we need to specify the types of the function’s parameters as well as the function’s return type. Remember, the purpose of this callback function is to deliver the information from the backend, so its parameter type will be an array of Post objects. This callback function receives these posts as an argument but does not return anything, hence the return type of Void.
Just to bring everything together, we declared a function as a parameter inside of fetchFeed called completion whose type is ([Post]) -> Void meaning it takes in one argument which is an array of Post objects and does not return anything.
You may have noticed the keyword @escaping. In the example earlier, when I asked my friend to deliver the gift to my parents, I am the original function. During that original function call, I asked my friend (the callback) to complete this task for me, but only want to ask them once and forget about it for now. We can think of the original function call being erased here. However, my friend still has a task to do, and I do not want them to forget (be erased). Connecting this back to Swift, we mark these callback functions (completion handlers) with the @escaping keyword so that it can outlive the original function. Otherwise, as soon as the original function is complete, the callback will be erased as well and we do not want that!
The complete network call could look something like this:
Spring 2024 | Vin Bui
Firebase Crashlytics is a real time crash reporting tool that helps us pinpoint crashes from our users. It makes is very easy to receive detailed insight on the events that led up to the crash, allowing us to quickly reproduce bugs and determine the root cause.
In order to set up Crashlytics, we must have a Firebase project configured for our app.
Under Project Settings > General, download the GoogleService-Info.plist file and drag it into our project directory.
Install the Firebase SDK via CocoaPods or Swift Package Manager and choose the libraries that we want to use (in this case FirebaseCrashlytics).
Follow the instructions from the to add the initialization code.
The does a pretty good job explaining how to set up Crashlytics so I won’t repeat it here. Follow the guide to add the SDK.
Note: If we are using CocoaPods as the package manager for our project, then there are 2 differences:
Firstly, skip Step 1 (adding Firebase through github and Swift Package Manager). The only thing we want to do from that step is check that Other Linker Flags does have -ObjC.
Secondly, when adding a new script to Build Phases, we need to make sure to use the script ${PODS_ROOT}/FirebaseCrashlytics/run instead of the provided script (which will search for a Swift Package Manager file that doesn’t exist)!
If we followed the documentation correctly, our app’s dSYM’s files should automatically be uploaded to Crashlytics, allowing us to read and process crashes.
Fall 2023 | Vin Bui
Similar to a UITableView, a UICollectionView is a sub-class of UIScrollView. In other words, it is also a scrollable view. However, a collection view is more dynamic and customizable than a table view. A collection view still contains sections, but instead of rows in a table view, it uses items. Each item is represented by a UICollectionViewCell and can be displayed in a grid-like manner. Additionally, a UICollectionView also supports horizontal scrolling.
A UICollectionView contains sections where each section contains items, and each item being represented by a cell. If we look at the Spotify example above, we can see 3 sections, each having its own header. For example, the top section has the header “Made For vinnie”. Now, each section contains items, which are represented by a UICollectionViewCell. In each section, the square image is a UICollectionViewCell. If you have used Spotify before, then you know that these sections are horizontally scrolled.
As we can see, a UICollectionView is a lot more versatile and customizable than a UITableView. It allows for horizontally scrolling as well as laying items out in grid-like manner.
Spring 2024 | Vin Bui
First, we want to change our app’s marketing version and build number. For consistency, use the following guidelines for minor updates:
1.1.1 → 1.1.2
1.1.9 → 1.2.0
1.9.9 → 2.0.0
Xcode Cloud handles build version, but keep the project updated with the next build version, which should always be incremented by 1, independent of the current version. If there is a major update, such as from Eatery to Eatery Blue, you can skip this versioning incrementation and change the first number completely (e.g X.0.0).
Before we get started with AppStore Connect, our app bundle must be archived and uploaded. The most common way to do this is to go to Xcode, then Product > Archive. Alternatively, we can use a CI/CD service such as Xcode Cloud that will automate this process for us.
Log in to our Apple developer account and select our target product.
In the TestFlight tab under Builds, select the archive and click through the dialogs for Export Compliance Information. For AppDev members, Vin automated this process for most of our apps so this can be skipped.
Encryption: Standard Encryption
Go to Apps -> <app_name> and select the Overview tab.
Click the + button next to iOS App to create a new app version matching the latest version number.
Under “What’s new in this version”, write down what changes were made (e.g. “Bug fixes and improvements.”)
Fall 2023 | Vin Bui
In the variables and constants section above, we assigned a text to a variable. In Swift, this is called a String and is one of the most important types we will use. However, there are many more types of data that Swift handles. In the example earlier, let’s try to do the following:
There are two ways we can fix this error:
Initialize the variable with a value when we create it.
Tell Swift what data type the variable will hold on later.
Fall 2023 | Vin Bui
If we want to execute a chunk of code only when a condition is met, then in Swift, we can use if, else if, and else statements. Let’s take a look at the following example:
When using conditionals, we must provide a condition which is an expression that evaluates to true or false. To enclose a block of code in Swift, we use curly brackets ({ and }). In the example above, the expression a == 0 evaluates to true so the block of code containing print("Zero") will be executed.
Fall 2023 | Richie Sun
Now that we know how to define class and utilize UIKit views, how do we position and organize these views in our apps? There are multiple ways to layout views to a screen in iOS development – some examples include frame-based, storyboards, and programmatic AutoLayout. However, the method we will be learning in this course is programmatic AutoLayout.
AutoLayout is a constraint-based organization system used for UI development in iOS applications. This constraint layout system allows for adaptive UI which adapts to screens of different sizes and orientations, using a relational layout structure to organize views with respect to one another. Thus, this method tends to be less error-prone and does not require us to worry about the coordinates of individual elements on the screen.
Fall 2023 | Vin Bui
Why does SwiftUI use structs for views? First, structs are simpler and faster than classes. But there’s more to it than just performance. In UIKit, every view is a subclass of UIView which is the superclass that contains hundreds properties and methods such as frame, backgroundColor, etc, that we don’t even use them all. You would then create a subclass of UIView and perhaps even a subclass of the subclass and you could keep going forever. Because classes can change their properties freely, things can get messy.
Fall 2023 | Vin Bui
Data persistence is the mechanism involving the storage of data on a disk without being altered by the user the next time the app is launched. In the iOS world, this primarily involves storing the data locally on the user’s device.
By default, the data that is allocated in our app is stored in RAM which is immediately cleared as soon as the user closes out of the app. For example, say we had an array of students displayed in a table view. Assuming there is no backend integration, when we launch the app, this array is stored locally in RAM. If we were to add a new student to this array during the app’s lifetime, we will notice that when we relaunch the app, the added student is gone.
Sometimes we want to save data on the user’s local disk so that when the user opens the app again, that data persists
Fall 2023 | Vin Bui
Tired of having to type the extremely long code for setting up auto-layout with NSLayout? SnapKit is here to make our lives easier! SnapKit is a wrapper library that makes setting constraints with auto-layout very simple.
You can use either Swift Package Manager or CocoaPods to install SnapKit. If using CocoaPods, simply write pod SnapKit in our Podfile then use pod install.
Fall 2023 | Vin Bui
We learned how to use variables and constants to store data, but only explored basic values such as integer numbers and text. However, when we program, we often need to hold more complicated data that requires a specialized format for organizing and retrieving the data. To do this, we use Data Structures.
The most common data structure that we will be using is an array. Arrays store a group of values together into a single collection, and we can access these values using their position in the array.
We use square brackets
git checkout origin/1-uilabel
OR git checkout 1-uilabel
git checkout origin/2-uiimageview
OR git checkout 2-uiimageviewclass Student {
// Properties
var name: String
var major: String
var age: Int
// Initializer
init(name: String, major: String, age: Int) {
self.name = name
self.major = major
self.age = age
}
}
// Creating an instance of Student
let jay = Student(name: "Jay", major: "CS", age: 67)
// Access properties like below
jay.name
jay.major
jay.age
// NOTE: the super class of `EngineeringStudent` is `Student`
class EngineeringStudent: Student {
// Inherits all properties and function from 'Student'
// Define more properties
var doesShower: Bool
init(name:String, major:String, age: Int, doesShower: Bool) {
self.doesShower = doesShower // Initalize property specific to this class
super.init(name: name, major: major, age: age) // Call the super class's initializer
}
}
// Creating an instance of EngineeringStudent
let asen = EngineeringStudent(name: "Asen", major: "CS", age: 22, doesShower: false)
asen.doesShower // This works fine since asen is an EngineeringStudent
jay.doesShower // This DOESN'T work since jay is a Student. `doesShower`
// is a property of EngineeringStudent but not Studentgit checkout origin/1-viewmodel
OR git checkout 1-viewmodel
git checkout origin/2-favoriting
OR git checkout 2-favoriting
git checkout origin/3-networking
OR git checkout 3-networking
git checkout origin/4-darkmode
OR git checkout 4-darkmode### Part II ###
git clone https://github.com/intro-to-ios/lec11-swiftui-2.git
OR git clone [email protected]:intro-to-ios/lec11-swiftui-2.git### Part II ###
git checkout origin/1-navigation
OR git checkout 1-navigation
git checkout origin/2-button
OR git checkout 2-button
git checkout origin/3-textfield
OR git checkout 3-textfield// Same function from above
func getData(handler: (String) -> Void) {
handler("Fetched Data")
}
func printData(data: String) -> Void {
print(data)
}
getData(handler: printData)The build should now be available on TestFlight. Make sure to choose a testing group.
Note that external testing requires app review approval whereas internal testing does not.
If there are new screenshots, upload the new screenshots. This part is somewhat tricky since the dimensions need to be perfect.
Add some promotional text, update description, etc.
Under Builds, select the targeted archive and click through the dialogs for Export Compliance Information: (you may not have to do this if you already did it through TestFlight)
Encryption: Standard Encryption
Available in France: No
Once done with the dialog, go to the top of the page and click Save then Add for Review. On the next page, select Submit for Review.
And now we wait… This can take a few business days. I recommend downloading the App Store Connect mobile app to receive notifications!









To generate an SSH Key, type ssh-keygen -t ed25519 -C "YOUR GITHUB EMAIL"
Press enter when asked: “Enter file in which to save the key”.
If asked “[…]/.ssh/id_ed25519 already exists. Overwrite (y/n)?”, type y and press enter.
Note: If you have previously configured SSH authentication for other services, overwriting your current key will likely cause you to lose access. You will need to reconfigure that service to authenticate using the new key in order to regain access.
When asked: “Enter passphrase (empty for no passphrase):” just press enter.
When asked: “Enter same passphrase again:” just press enter.
Copy the generated key to your clipboard by typing pbcopy < ~/.ssh/id_ed25519.pub
Go back to the GitHub page from earlier. For the Title field, I recommend putting the name of your device (such as “Vin’s Macbook”). The key type is Authentication Key. Paste the generated key, then click Add SSH key.
main
// Same function from above
func getData(handler: (String) -> Void) {
handler("Fetched Data")
}
getData(handler: { data in
print(data)
})If we take a closer look at the body property of our struct, we can see three different views: VStack, Image, and Text. We will learn more about these views in detail later. You may notice that these views have methods attached to them, such as .padding(). These are known as modifiers. Modifiers return a new view which is an exact replica of our original but with the extra modification. For example, the .padding() modifier on the VStack creates a duplicate of the original VStack but with extra padding. In other words, everything is a view!
Now, notice how these modifiers create a new view. Remember from earlier how the body property has a return type of some View? Well, every time we are adding a modifier to our view, the type that is returned by the property is different. To explore this a bit deeper, we created a Button view that prints out the return type of body when tapped. This Button view has the .padding() and .frame() modifiers added onto it.
This was the output created when we tapped on the button:
ModifiedContent<ModifiedContent<Button<Text>, _PaddingLayout>, _FrameLayout>
Just from looking at this, we can see that the type contains the modifiers that we added onto our view. Now, imagine having a bunch of views, each with different modifiers. The return type can be extremely long! This is the exact reason why the view properties in our struct have a return type of some View.
In Swift, we can use the following tools and APIs to achieve data persistence:
UserDefaults
File System
Keychain
CoreData
SwiftData
Property Lists
In this course, we will only be looking at UserDefaults.
UserDefaults allows us to store small pieces of data on a user’s local drive. Reading and writing to user defaults is very simple since it uses key-value pairs, just like a dictionary.
To write to UserDefaults, simply follow this format:
To read from UserDefaults, follow this format:
One thing to keep in mind when reading from UserDefaults is the type of the returned value. For example, when reading in a String, we will receive a String? type.
You can also store custom objects in UserDefaults; however, that requires using an encoder/decoder. For the scope of this course, we will only store simple pieces of data.
The best way to understand how to use SnapKit is to compare it with the code that we have written so far.
SnapKit offers a lot more than just these basic constraints so feel free to Google! We also do not have to set translatesAutoresizingMaskIntoConstraints to false when using SnapKit!
// NSLayout
NSLayoutConstraint.activate([
child.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
child.topAnchor.constraint(equalTo: parent.topAnchor),
child.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
child.bottomAnchor.constraint(equalTo: parent.bottomAnchor)
])
// SnapKit (note that we do not need commas)
child.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
}
// Or we can do the following
child.snp.makeConstraints { make in
make.leading.top.trailing.bottom.equalToSuperview()
}
// Or even the following
child.snp.makeConstraints { make in
make.edges.equalToSuperview()
}// Using SafeAreaLayoutGuide
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)(base) vinnie@dhcp-vl2041-37760 ~ % cd Desktop
(base) vinnie@dhcp-vl2041-37760 Desktop %git checkout -b "BRANCH_NAME"git merge maingit reset HEAD~2git push --forcegit pull <options> <repository>git statusgit add <changes to be staged>git commit -m <"Commit Title">git push origin maingit branchgit checkout -b <"new branch name">
or
git checkout <existing branch name>func fetchFeed(completion: @escaping ([Post]) -> Void) { }func fetchFeed(completion: @escaping ([Post]) -> Void) {
var posts: [Post] // The list of posts
// 1. Send a request to the backend (ex: GET request)
// 2. Handle the backend response (error handling, decode JSON data)
// 3. Assign the variable `posts` with the decoded data
completion(posts) // 4. Give the callback function the list of posts
}var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}var body: some View {
Button("Hello!") {
print(type(of: self.body))
}
.padding()
.frame(width: 200, height: 200)
}UserDefaults.standard.setValue(_ value: Any?, forKey key: String)
// For example, to store the string "Vin" for key "name", we can do...
UserDefaults.standard.setValue("Vin", forKey: "name")UserDefaults.standard.string(forKey: String)
UserDefaults.standard.bool(forKey: String)
UserDefaults.standard.integer(forKey: String)
UserDefaults.standard.array(forKey: String)
UserDefaults.standard.dictionary(forKey: String)
// Using the example earlier, to access the value stored in key "name"...
UserDefaults.standard.string(forKey: "name") // Returns "Vin"
// Note that we use `.string` since we stored a String type for this key// NSLayout with insets, height/width, and relative to other subviews
NSLayoutConstraint.activate([
childTwo.leadingAnchor.constraint(equalTo: childOne.trailingAnchor, padding: 16),
childTwo.trailingAnchor.constraint(equalTo: parent.trailingAnchor, padding: -16),
childTwo.centerYAnchor.constraint(equalTo: childOne.centerYAnchor),
childTwo.heightAnchor.constraing(equalToConstant: 96)
])
// SnapKit Version
childTwo.snp.makeConstraints { make in
make.leading.equalTo(childOne.snp.trailing).offset(16)
make.trailing.equalToSuperview().offset(-16) // or .inset(16)
make.centerY.equalToSuperview()
make.height.equalTo(96)
}This is a 2 credit S/U course, but you may enroll for 1 credit to avoid going over the credit limit. If you haven’t already enrolled in the course:
Enroll in CS 1998-601 in Student Center.
Lectures are Monday & Wednesday 8:35 - 9:25 PM in Olin Hall 165.
Our (tentative) course schedule can be found here:
CS 1110 is a highly recommended co/prerequisite, but not required. You will also need access to a MacBook running at least Catalina and Xcode 10.14 to participate in the course (the Xcode IDE is only available for macOS).
You can check your Xcode version via the Terminal or in Xcode itself. To check within Xcode, navigate to the Menu Bar -> About Xcode. To check using Terminal, see the screenshot attached!
This entire course is project-based meaning there will not be any exams. There will be a Hack Challenge at the end of the course where you will work with members from our backend and design courses to put what you’ve learned to the test and build your very own mobile app. More information will be provided later.
There are no required textbooks. Most of the information you need will be in this course textbook. However, you are welcome to consult other iOS development resources such as Hacking with Swift or iOS Academy on YouTube.
All lecture slides will be posted in this textbook under “Chapters” on the sidebar. Lectures will also be recorded and posted on the AppDev YouTube channel. The demo code is located in this GitHub.
All course-wide announcements will be made on Ed Discussion.
Our grading policy can be found here:
Attendance will be taken at lectures and will be worth 5% of your final grade. However, in the event that you cannot make them, lectures will also be recorded and uploaded to our YouTube channel.
As with any other course at Cornell, the Code of Academic Integrity will be enforced in this class. All University-standard Academic Integrity guidelines should be followed. This includes proper attribution of any resources found online, including anything that may be open-sourced by AppDev. The University guidelines for Academic Integrity can be found here.
When you work on an assignment, you have the option of working with a partner who can help you work on the assignment without any limitations. You are also free to come to the instructors or any course staff for help. Programming forums like Stack Overflow or Hacking with Swift are allowed as long as you understand the code and are not copying it exactly.
The majority of code (excluding external libraries) must be written by you or your partner. Code written through AI means such as ChatGPT is NOT ALLOWED. However, you may use these resources for assistance, although we highly encourage consulting Ed Discussion or office hours instead.
This schedule is tentative and subject to changes to Cornell's academic calendar.
1
Wed 10/15
L1: Course Logistics + Swift Basics
Installation & Setting up A1 Released
2
Mon 10/20 Wed 10/22
L2: UIKit + AutoLayout
L3: MVC + Navigation + Delegation
(10/24) A1 Due (10/23) A2 Released
3
The key takeaway here is that Swift is a statically typed language, meaning that the type of every property, constant and variable that we declare needs to be specified at compile time. This is a good thing because it prevents us from putting anything inside of the “box”. This is known as type safety. Let’s demonstrate this by introducing a new data type Int (integer).
Everything works fine, but what if we were to swap the values of instructor and year?
Our code no longer compiles because we tried assigning a value whose type does not match the type of the variable at the time it was created.
We can store decimal numbers by using a Float and Double. The difference between these two is that a Double has twice the accuracy of a Float, and hence takes up more storage.
A Bool (boolean) in Swift is a data type that can hold one of two values: true or false.
Earlier when we assigned an initial value to a variable,
Swift automatically infers what data type the variable will hold. This is known as type inferencing. We could also specify a data type and provide an initial value at the same time:
Most of the time, we will not be using type annotations and prefer having Swift infer our types. However, there are times when type annotation should be used. This will come with practice and from seeing how we write our code.

a == 0 evaluates to false? In that case, Swift will read for the next condition, if any. Because the next statement is an else statement, there is no condition to check so this block of code will be executed.Sometimes, we want to check for multiple conditions? In that case, there is more than one option:
Use the && (and) or || (or) operators
Use an else if statement
Let’s take a look at the second option.
First, the expression a < 0 is evaluated. Since -6 < 0 evaluates to true, Swift executes the block of code containing print("Negative"). Now, since the next statement is an else if statement, it will not be executed. The reason is because this else if statement is connected to an if statement. Since the first if statement evaluated to true, Swift will not check any other statements that are connected.
Now, if we were to change the else if statement to an if statement, Swift will check this statement because it is no longer connected to the first if statement.
Because both conditions are met and they are not linked together, "Negative" and "Even" will both be printed.
Sometimes we want to exit our code execution early on for efficiency purposes. This is where the guard statement comes in. A guard statement is similar to an if statement except an if statement runs when the condition is true while a guard statement runs when the condition is false. We can think of a guard statement as using an if statement with a “not equals” (!=) or not” (!) operator.
The format for a guard statement is as follows:
condition is an expression that evaluates to true or false. If true, then the block of code is not executed. If false, then the block of code is executed. This is the exact opposite of an if statement.

When dealing with AutoLayout and constraints, there are a few terms that are important to understand before jumping in:
Superview:
The superview is the immediate ancestor of the current view. In other words, it is the view that the current view is contained within.
Subview:
Subviews are the children of the current view. In other words, they are the views which are contained within by the current view.
Constraint:
In general, constraints must define both the size and the position of a view, in order for that view to display properly within its superview. Think of them as the support beams that keep a view in place.
Anchors:
Every UIView has a set of anchors that define its layouts rules. The most important ones are: widthAnchor, heightAnchor, leadingAnchor, trailingAnchor, topAnchor, bottomAnchor, centerXAnchor and centerYAnchor (Examples in image below)
Before we begin setting up anchors and constraints, the view needs to be added to the base view of the NavigationController, or any superview. To do that, we call the following function:
In the code chunk above, superview is the view that we want to contain the currentView
Every subview of UIView has these four properties: topAnchor, leadingAnchor, bottomAnchor, and trailingAnchor. As the names imply, topAnchor refers to the view’s top edge, leadingAnchor refers to the view’s left edge, bottomAnchor refers to the view’s bottom edge, and trailingAnchor refers to the view’s right edge.
An IMPORTANT note to always remember is that in order for us to use these anchors to create constraints (layout our views), we must remember to set the view’s translatesAutoresizingMaskIntoConstraints property to be false before setting these anchors.
Suppose we want to constrain a UILabel called labelA 50 pixels from the top of the screen, and horizontally aligned to the center of the screen like shown below:
This is how we would do that:
git clone https://github.com/intro-to-ios/lec2-uikit
OR git clone [email protected]:intro-to-ios/lec2-uikit.git[],Swift uses type inferencing to determine the type of staff. Because all of the elements inside of the array are strings, Swift knows that staff is an array of strings (Array<String>). If we change the value of an element to a different type, our code will not compile.
Instead of letting Swift infer what types our array will hold, we can specify the type that we want.
As we can see, if we put in a value that does not match with the given type, our code will not compile.
However, it is possible to allow our arrays to hold any type. We can give it the special Any data type:
When adding values to our array, we must first initialize it with an original value. The following code will not compile:
We can initialize our array in the following ways:
Notice that we used an append method to add elements to the end of the array. Swift provides many methods that we can use on our array, and we can even add our own! We can also use operators such as + to glue arrays together and return a new array. Read more about them in the Apple Documentation.
Another common data structure that we might encounter are called dictionaries. These are similar to arrays except we use a key to access a value in the collection. In other words, dictionaries store key-value pairs.
It is also common to break up our dictionary like so to keep things readable:
Similar to arrays, Swift provides a lot of methods that we can use with dictionaries. The Apple Documentation provides more information about them.

git clone https://github.com/intro-to-ios/lec4-uitableview.git
OR git clone [email protected]:intro-to-ios/lec4-uitableview.gitgit checkout origin/1-cell
OR git checkout 1-cell
git checkout origin/2-tableview
OR git checkout 2-tableviewFall 2023 | Vin Bui
When we want to repeat a code a certain number of times in Swift, we can either copy and paste the code or even better, we can use loops. There are two main loops in Swift: a for loop and a while loop.
Let’s say we wanted to print out the numbers 1..10. In Swift, we can use the closed range operator (...) which is three periods in a row.
The variable i is known as a loop variable which is a variable that lives within the scope and lifetime of the loop. For every iteration, the value of i will change. Now, what if we didn’t need to use i and just wanted to print "Hello Vin and Richie" 10 times? We could still use for i in 1...10; however, it would be better to use an underscore (_) instead.
Why is ... called a closed range operator? Well, that’s because there is also an open range operator (..<). The difference between these two is that the closed range operator is inclusive whereas the open range operator is not. The following code will only be executed 9 times. It goes up to but not including 10.
Swift provides a nice way to loop over the elements of an array using the for-in loop.
In this code, the loop variable is person. For every iteration of this loop, the value of person will be the value of every element inside of the array staff, in order.
Instead of looping over the element of the array, we could have looped over the indices of the array. The following code is equivalent:
If we don’t know exactly how many times to repeat a block of code, but do know that we want to repeat it while a condition is true, then we can use a while loop.
The above code will print out the value of i and increment the value of i by 1 while i < 10 evaluates to true.
However, be very careful when using while loops because we can create an infinite loop. In the code below, the value of i never changes and will always be less than 10. In this case, there will be an infinite loop:
Fall 2023 | Vin Bui
So far, the data that we have used have been hard-coded on the frontend. For example, in our A3: ChatDev assignment, the Post objects that are displayed in the table view are hard-coded inside of our code. There is an array of Post objects written in Swift code known as dummy data. In other words, these posts are expected to be fetched from the internet and displayed onto our app. If a different user creates a post, then we want it to be updated on our side.
A client is a device or software that requests a service. In this course, the client is our iOS app because it requests information from some service. Who provides this service? That’s the job of a server. A server is a device or software that responds to clients with these services. Remember, clients send a request and a server responds.
An HTTP request is a method used to communicate between a client and a server. There are many different types of HTTP requests; however, the most common types are GET, POST, PUT, and DELETE. For the scope of this course, we will only discuss a GET and a POST request (and maybe DELETE).
These requests are commonly paired with a CRUD operation. CRUD stands for CREATE, RETRIEVE, UPDATE, and DELETE. This idea is often used when dealing with models. For example, we can retrieve posts on Instagram, create a post on Instagram, etc.
So, the client sends an HTTP request to a server asking for some service. What can the server respond with? Well, the server can respond with the service the client is requesting, but we don’t live in a perfect world and things can go wrong! For example, when we click on a website, as clients, we are sending an HTTP request to the web server to provide us with the website information. But what if our WiFi connection gets cut as we are sending the request? How will the server communicate with us about what went wrong?
An HTTP response code (or status code) indicates whether an HTTP request has been successfully completed and provides information about the request. Have you ever went to a website and received a “404 Not Found” error? Well, this “404” is an HTTP response code indicating that the server could not find what the client was looking for.
HTTP response codes range from 1XX to 5XX, with the first digit indicating the category:
So far, we’ve learned that clients send an HTTP request and a server responds back with a status code. However, clients and servers need to understand the data that is being transmitted. The issue is that most of the time, client-side and server-side code are written in different languages. For example, our code may be written in Swift, but the backend server may not be able to recognize it. Then, how are we able to send this data? We use JSON.
JSON (JavaScript Object Notation) is a text-based data format that is language-independent. The powerful thing about JSON is that it is widely used and many modern programming languages and frameworks have built-in functionality to parse data in JSON format. JSONs have key-value pairs, just like dictionaries in Swift. The keys are always strings, but a value can be a string, number, object, array, boolean, or null.
Consider the following Student model:
In Swift, if we wanted to create this object, we could write the following code:
Now, this object is currently stored locally on the frontend. If we wanted to fetch this from the backend, then the server response can represent the data as a JSON (notice that this is very similar to a dictionary in Swift):
Fall 2023 | Vin Bui
Enter the name of our app. In this example, we will be calling our app Sample.
Choose SwiftUI for the Interface.
On the left side of Xcode, we shoul see a list of files in the directory. This is known as the project navigator.
SampleApp.swift contains code that is executed when we launch our app. If we want to create something when we launch our app and keep it throughout the app’s lifetime, we will write it here.
ContentView.swift contains code that defines the UI for our app. Most of our code will be written in a file similar to this.
Assets.xcassets is a catalog that contains the app’s images, icons, colors, and more.
On the right side of Xcode, we should see a canvas containing a preview of the app. If not, go to the Editor menu and select Canvas (alternatively ⌥ ⌘ ↵). The device that is shown on the preview depends on the device we select at the top center of Xcode. Because the preview is updated live, errors within our code can cause the preview to pause. To resume, we can click Resume in the canvas or even better, use the shortcut: ⌥ ⌘ P
ContentView.swiftTo use all of the functionality provided by the SwiftUI framework, we must import it using import SwiftUI. This is similar to how we import other frameworks such as UIKit, MapKit, etc.
Next, we create a struct called ContentView that conforms to the View protocol. The View protocol is a type that represents our app’s UI and provides modifiers to configure our views. If we want to draw anything on the screen using SwiftUI, then it must conform to the View protocol.
Inside of the ContentView struct, there is a required property called body that has the type of some View. What this type really means is that this property will return something that conforms to the View protocol. Later on, we will learn that the actual return type of this property is very complicated (could be 1000+ characters) so providing a general return type of some View means we can ignore that. The body property is the only requirement needed to conform to the View protocol.
Starting in iOS 17, the syntax for the preview is different:
The functionality is the exact same as it was in previous iOS versions.
Fall 2023 | Reade Plunkett
In this project, we will create a Weather Widget that can show up to 6 six different weather conditions. The conditions will change based on a location you input into the widget. Additionally, this widget will scale to look great in a small, medium, and large variant. As well, it will adapt to be viewable as a lock screen widget and standby widget on iOS. It will also be available on iPadOS and macOS. We will also use App Intents to allow this widget to be configurable and let the user input a location and have the weather and widget display update.
Create a new app. While you can use either SwiftUI or UIKit for your app, your Widget can only be built in SwiftUI.
Our Widget will live in a Widget Extension. Create a new Widget by selecting File → New → Target → Multiplatform → Widget Extension.
You can specify the platforms you want this widget to be available on. We want this widget to work on all platforms available, so we will select Multiplatform.
A new directory will be generated inside of your project navigator:
WeatherWidgetBundle.swift is the main entry-point for our widget. If we have multiple widgets for our app, we can declare them here within a WidgetBundle.
WeatherWidget.swift contains the majority of code that defines the Widget. It contains four components, the widget extension’s TimelineProvider, its TimelineEntry, its View, and the Widget
On the right side of Xcode, you should see a canvas containing a preview of the app. If not, go to the Editor menu and select Canvas (alternatively ⌥ ⌘ ↵). The device that is shown on the preview depends on the device you select at the top center of Xcode. Because the preview is updated live, errors within your code can cause the preview to pause. To resume, you can click Resume in the canvas or even better, use the shortcut: ⌥ ⌘ P
The canvas shows the default widget generated by Xcode on the home screen. Beneath, you can see a visualization of the timeline for this widget. We will discuss the Widget’s timeline in more depth shortly. Feel free to play around with the Widget’s View inside of WeatherWidget.swift and see how the live preview responds!
While it is nice to have this boilerplate code generated for us, we want to start things off from scratch to better understand how Widgets are built.
Delete all of the code inside of WeatherWidget.swift. Eventually, this file will contain the widget and its configuration.
Create 4 new Swift files:
Weather.swift: This file will contain the Weather model to hold our data.
Spring 2024 | Vin Bui
As developers, we are often faced with bugs that require us to debug our code. The most common way to do this (and the way it is taught in many CS courses at Cornell) is to use print statements. Although print statements are a very powerful tool to help debug our code, we can go beyond that and use a library called OSLog.
OSLog is Apple’s recommended library for logging and is a replacement for print statements and NSLog. OSLog allows us to mark different logging levels such as “warning” and “error” as well as structuring our app’s logging into categories that we can create. Additionally, it works perfectly with Xcode 15’s new logging console, enriching our debugging experience even further.
To get started, let’s create a file in our project directory called Logger.swift. Inside of the file, add the following code:
We first import this library with import OSLog in order to use all functionality provided by it.
We then create an extension of the Logger class which is already defined in OSLog.
Every Logger instance requires two things: a subsystem and category.
To write a log, we can use the following syntax:
We use the static variable services that we created earlier (which is a Logger object), followed by a log level (in this case info). There are many log levels that we can choose from, each being displayed differently in the Xcode console.
default (notice): The default log level which should be avoided. Our logs should be more specific.
info: Log information that may be helpful but is not necessary for debugging.
debug: We use this level during development while actively debugging.
We also need to make sure that our console has the proper Metadata selected to display the logs properly:
When logging, we need to consider data privacy. For example, if we wanted to log a user’s birthday, we can use the following code:
We use string interpolation to output the value of the birthday constant and mark it as private. We want to do this to prevent external apps such as Console to be able to view sensitive information in our logs.
For more advanced logging, we can use the Console app to read our logs. The Console app supports optimized logging structures with alignments which improves readability. Additionally, we can filter logs from multiple devices, categories, and log levels. Learn more about logging in the .
Fall 2023 | Vin Bui
So far, we've only worked with one screen which may contain many views. For this single screen, we controlled the models and views with a single UIViewController. However, many of the apps that we use today contain multiple screens that we can navigate between. In UIKit, we represent every distinct screen with a separate UIViewController. There are two ways to navigate between screens: 1) pushing/popping and 2) presenting/dismissing.
Before we dive in to the technical details, let’s observe what pushing and popping looks like.
As we can see, when we tap on one of the cells, a new screen shows up. Each new screen that we push is a separate UIViewController. So then, how do we keep track of the view controllers that have been pushed to figure out which one needs to be popped? We use a navigation stack. Every time a view controller is pushed, it goes on top of the navigation stack. Think of the navigation stack as a stack of books where each book is a view controller. The last item to be pushed into this stack will be the first one to be popped out (LIFO). How do we represent this navigation stack in UIKit? We use a UINavigationController.
Inside of SceneDelegate.swift add this code to the function scene:
The important step to remember here is step 3 where we first initialize the view controller (rootVC) that we want to be displayed when the app launches. Now, we have to place rootVC into the navigation stack by creating a UINavigationController with rootVC as the root view controller.
One thing to keep in mind is that the line let rootVC = ViewController() creates a view controller whose class name is ViewController. When we create a new project, by default, a class called ViewController is created for us. If we created a new class or renamed the class to HomeViewController, then we would use HomeViewController() instead.
Now, to push and pop a view controller is very simple:
UIViewController will be the view controller object that we want to push. Most of the time, we want to set animated to true. For example, if we had a class called ProfileViewController and wanted to push it, we would write the following code in the view controller class that is pushing it (not inside ProfileViewController):
To pop the ProfileViewController, we would write this code in ProfileViewController:
We could then link this code to some action such as a tapping on a button, cell, image, etc.
Let’s observe what presenting and dismissing looks like.
As we can see, a modal sheet is presented from the bottom of the screen and gradually transitions up. This is presenting. To dismiss, we can simply click on the cancel button or more commonly, swipe downwards from the top of the modal sheet. The view controller that is being presented is displayed on top of the previous view controller. There is not a navigation stack at play here ⇒ No UINavigationController.
To present/dismiss a view controller:
For example, if we had a class called ProfileViewController and wanted to present it, we would write the following code in the view controller class that is pushing it (not inside ProfileViewController):
To dismiss the ProfileViewController, we would write this code in ProfileViewController:
As we may have notice from the function header above, there is an optional parameter called completion. This is known as a completion handler which is a function that gets executed when the function call is complete. We will discuss this in detail once we get into networking.
Fall 2023 | Vin Bui
Imagine a large scale application with thousands of lines of code. The codebase would be very messy! To solve this, we need to be able to reuse our code. We can do this with functions.
Functions allow us to define reusable blocks of code. We define a function by using the func keyword followed by the name of the function (myName) and open/close parentheses:
If we were to just define this function in the playground, nothing will be printed out. This is because we also need to call the function. We can call the function we previously defined with the following code:
Let’s test this in the playground:
The nice thing about functions is that we can pass in arguments to make our functions a lot more useful. Using the example above, let’s customize our function to make it a lot more versatile:
This function has a parameter called name which is of type String and uses string interpolation to output the name. We would then need to pass in an argument to the function call:
In Swift, we can change the way parameters are named in the function call and inside of the function definition.
In this example, the name of the parameter within the function definition is str but when we call the function, we use name. str is known as an internal parameter and name is called an external parameter. This may not seem useful at first glance, but it is a very powerful feature once we begin writing code.
We can also use an underscore (_) as the external parameter.
By doing this, we do not need to provide the external parameter name when passing in our argument in the function call.
The functions that we defined earlier did not have any return value, meaning that when we called the function, nothing gets sent back to the function caller. However, many of the functions we create will have a return value. To do this in Swift, we use the right arrow (→) followed by the return type.
The function above will return true if the argument that we pass in is an even number and false otherwise.
Because this function returns a value, we can do many things with this function call such as assigning the returned value to a variable.
Fall 2023 | Vin Bui
When we create apps, we don’t just care about creating our views — we also want to lay them out. In SwiftUI, there are three main ways to lay out our views:
HStack
VStack
ZStack
An HStack is a view that arranges its views in a horizontal line. For example, to lay out the text “Cornell” and “AppDev” horizontal, we can use the following code:
A VStack is a view that arranges its views in a vertical line. For example, to lay out the text “Cornell” and “AppDev” vertically, we can use the following code:
A ZStack is a view that overlays its subviews, aligning them in both axes. For example, to place the text “AppDev” above a background color of red, we can use the following code:
A Spacer is a flexible view that expands along the major axis of its containing stack layout, or on both axes if not contained in a stack. For example, to place the text “Cornell” as far away from “AppDev” in an HStack, we can use the following code:
Of course, there are other types of views we can use to lay out our views. I recommend reading the Apple Documentation for each one if you’re interested.
LazyHStack
LazyVStack
List
ScrollView
Spring 2024 | Vin Bui
When creating any application, it’s very important that we ensure our apps function properly and maintains that proper functionality for as long as possible. However, this can be quite difficult when working with larger codebases and more complex applications as adding new code may cause undesirable changes to our app. To maintain functionality, we often write unit tests to ensure that our code works properly after making changes to our codebase. In the iOS world, we typically write unit tests for the smallest, yet important, functions that control the logic of our app.
There are multiple ways to create our testing suite.
var instructor
instructor = "Vin"var instructor: String
instructor = "Vin"var instructor: String
instructor = "Vin"
var year: Int
year = 2025var instructor = "Vin"var instructor: String = "Vin"var a = 0
if a == 0 {
print("Zero")
} else {
print("Not Zero")
}var a = -6
if a < 0 {
print("Negative")
} else if a % 2 == 0 {
print("Even")
} else {
print("Not even or negative")
}guard condition else {
// block of code
// control statement: return, break, continue, or throw
}superview.addSubview(currentView)let labelA = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// Don't forget this step!!
labelA.translatesAutoresizingMaskIntoConstraints = false
// We need to add labelA to the base view of the Navigation Controller
view.addSubview(labelA)
// We have to activate our constraints in order to
NSLayoutConstraint.activate([
// Here, we can insert all the constraints that we want activated
labelA.topAnchor.constraint(equalTo: view.topAnchor, constant: 50)
labelA.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}git checkout origin/1-uilabel
OR git checkout 1-uilabel
git checkout origin/2-uiimageview
OR git checkout 2-uiimageviewclass Student {
// Properties
var name: String
var major: String
var age: Int
// Initializer
init(name: String, major: String, age: Int) {
self.name = name
self.major = major
self.age = age
}
}
let vin = Student(name: "Vin", major: "Info Sci", age: 20)
vin.major // Prints "Info Sci"
class EngineeringStudent: Student {
// Inherits properties from the superclass, but you can
// define other properties specific to this class
var doesShower: Bool
// Initializer
init(name: String, major: String, age: Int, doesShower: Bool) {
self.doesShower = doesShower // Initialize properties specific to this class
super.init(name: name, major: major, age: age) // Call superclass' initializer
}
}
let archit = EngineeringStudent(name: "Archit", major: "CS", age: 20, doesShower: false)
archit.name // Good
archit.doesShower // Good
vin.doesShower // Does not work
var staff = ["Vin", "Richie", "Tiffany", "Jennifer", "Antoinette", "Elvis"]
staff[0]
staff[1]
staff[2]var staff: [String] = []
var staff = [String]()var staffAge: [String: Int] = ["Vin": 19, "Richie": 20, "Antoinette": 4]### Part I ###
git clone https://github.com/intro-to-ios/lec10-swiftui-1.git
OR git clone [email protected]:intro-to-ios/lec10-swiftui-1.git
### Part II ###
git clone https://github.com/intro-to-ios/lec11-swiftui-2.git
OR git clone [email protected]:intro-to-ios/lec11-swiftui-2.git### Part I ###
git checkout origin/1-layouts
OR git checkout 1-layouts
git checkout origin/2-list
OR git checkout 2-list
git checkout origin/3-refactor
OR git checkout 3-refactor
### Part II ###
git checkout origin/1-navigation
OR git checkout 1-navigation
git checkout origin/2-button
OR git checkout 2-button
git checkout origin/3-textfield
OR git checkout 3-textfieldfor i in 1...10 {
print(i)
}func myName() {
print("My name is Vin")
}myName()Mon 10/27 Wed 10/29
L4: UITable View
L5: UICollectionView
(10/30) A2 Due (10/30) A3 Released
4
Mon 11/3
Wed 11/5
L6: Networking I
L7: Networking II
(11/5) A3 Midpoint Due
5
Mon 11/10 Wed 11/12
L8: Persistence + SnapKit
L9: SwiftUI I
(11/10) A3 Final Due (11/10) A4 Released
6
Mon 11/17 Wed 11/19
L10: SwiftUI II
L11: MVVM
(11/17) A4 Mid Point
(11/18) Hack Challenge mixer!
(11/20) Hack Challenge Starts
7
Mon 11/24 Wed 11/26
L12: Core ML
THANKS GIVING BREAK!!
(11/25) A4 Due
8
Mon 12/01 Wed 12/03
L13: App Deployment
L14: AppDev + Career talk
(12/05) Hack Challenge Submissions Due
(12/08) Attend Hack Challenge Final



















Assets.xcassets is a catalog that contains the widget’s images, icons, colors, and more. This is separate from your app’s own xcassets catalog.
Info.plist is a list of metadata relevant for our widget. It can contain appearance options and various other settings.
WeatherWidget.entitlements is a list of special capabilities this widget requires. It contains key-value pairs that grant an executable permission to use a service.
WeatherWidgetEntry.swift: This file will contain the definition for an entry of our widget.
WeatherWidgetProvider.swift: This file will contain the Widget’s timeline provider.
WeatherWidgetView.swift: This file will contain the SwiftUI view for our widget.
In the next section, we will start writing these files and building our widget. For now, ensure your project navigator looks as such:






A2
15% + 2%
A3 Midpoint
5% (completion)
A3 Final
15% + 3%
A4 Midpoint
5% (completion)
A4 Final
15% + 5%
Assignments Subtotal
65% + 10%
Hack Challenge
30%
Attendance
5%
Total
100%
Extra Credit
+ 0-10%
Passing Score
70%
Assignments are due at 11:59pm, but we will continue to accept submissions for 2 days. For example, if the assignment is due Tuesday, the last day we will accept the submission is Thursday.
You are given a total of 4 free slip days. After all free slip days have been used, there will be a 10% deduction from that assignment’s grade per day submitted for a maximum of 2 days. In other words, no late submissions will be accepted the third day after the normal submission deadline. Midpoint submissions do not count towards your slip days. If there are any emergencies or other conflicts out of your control that prevent you from turning in your assignments on time, please reach out to the instructions so we can help you.
There will be a total of 4 assignments throughout the duration of the course. The weighted percentage for each submission is displayed in the table above.
All final submissions (A1, A2, A3 Final, A4 Final) will be graded for correctness. However, A3 Midpoint and A4 Midpoint will be graded for completion. As long as you have shown some progress on GitHub, you will receive full credit. Although these midpoint submissions are for completion, we highly recommend that you take these midpoints seriously so that you do not fall behind. We will provide feedback on midpoint submissions if requested to make sure you are on the right track
You are allowed to work with one other person in the course for A2, A3, and A4. However, A1 must be submitted individually. We will be using CMS for grading and submission. The submission requirements and details for each assignment will be provided later.
Assignments will be graded and returned, at the latest, one week after the normal submission deadline. If you feel that the grader make a mistake, create a private Ed post with an explanation about the mistake.
You will need to put in effort in order to pass this class. We will send out emails to students in jeopardy before the drop deadline. If you have any questions or concerns, please reach out to the course instructors. We care about your learning and want everyone to succeed!
There will be extra credit opportunities for every assignment where you can go above and beyond the minimum requirements. These will be challenging but are very rewarding and will definitely help you become a better developer. You can earn up to a 2% boost for A2, 3% boost for A3 Final, and 5% boost for A4 Final, adding up to a total of 10%. Details will be provided in the assignment handout.
This final group project is weighted more heavily than the rest of the individual assignments, so if you don’t do so great on the assignments, a solid final project can boost your grade significantly.
More details coming soon!
A1
10%






There is a group labeled Preview Content that contains a single file called Preview Assets.xcassets. This is a catalog similar to Assets.xcassets above, but for previewing the UI which will be explained next.



We define a static variable representing our subsystem which should be unique. The best way to unsure uniqueness is to use our bundle identifier.
We can then create our Logger instances with a category of our choice. The categories shown above are some common ones.
warning: Warning-level messages that do not cause any failure or critical errors.
error: Error-level messages for critical errors and failures.
fault: Fault-level messages for system-level or multi-process errors.
critical: Same as fault.








LazyHGrid
LazyVGrid
Form
Divider




If creating a brand new project, we can check the box labeled Include Tests.
For an existing project, go to File > New > Target > Unit Testing Bundle. Select the project name and the target to test.
In this chapter, we will be writing unit tests for an app called Sample. Notice that a new group gets created outside of our original project. In our case, it is called SampleTests. In this folder, we will create a new Swift file called SampleTests.swift.
In a larger, more realistic application, it’s more likely that we will have multiple screens/views. In this case, it’s common to create multiple test files, one for each screen. For example, if our Sample app contained a home page, we can have a HomeSampleTests.swift file. This keeps our codebase neat and easier to read for other developers.
Let’s analyze the code below:
In order to use Xcode’s testing functionality, we must import the XCTest library.
We import the name of our main module, which in this case is Sample. Note that we must mark it with @testable.
We create the class representing our test suite. The name of this class should match the name of our file. This class must conform to the XCTestCase protocol and is marked with final to prevent any unwanted changes to the instances of this class.
Let’s say we wanted to test a function that counts the number of even numbers in an array. If this function is called countEvens, we can call our testing procedure testCountEvens.
However, this code assumes that the Sample module that we imported has a publicly accessible function called countEvens. If this function was declared as a method inside of a class, we must first instantiate an object of that class and then call the function.
In our example, we are testing for equality so we use the XCTAssertEqual function, and pass both values as arguments.
There are multiple ways to run the test procedure. The most simple way is to click on the play button next to the name of the function we just created. If we want to run the entire testing suite, we can click on the play button next to the name of our testing class.
Xcode will then build our application and launch the Simulator. If our build succeeds, there will be a popup stating Build Succeeded. Note that this DOES NOT mean that our test cases have passed. If our test cases have passed, there will be a Green checkmark next to each assertion call. Otherwise, there will be a Red cross indicating a failure.
The difficult part in writing unit tests is knowing when to write them. It would be OD to test every single function in our entire codebase, so it’s important that we only test functions that contain complex logic. For example, if we had a function that removes spaces from a String, that may be unnecessary because we can see that when viewing the app. However, if we had a more complex function that contains many edge cases and is difficult or impossible to view through the app, then writing unit tests can help us handle those unseen cases.
for _ in 1...10 {
print("Hello Vin and Richie")
}for _ in 1..<10 {
print("Hello Vin and Richie")
}var staff = ["Vin", "Richie", "Tiffany", "Jennifer", "Antoinette", "Elvis"]
for person in staff {
print(person)
}var staff = ["Vin", "Richie", "Tiffany", "Jennifer", "Antoinette", "Elvis"]
for i in 0..<staff.count {
print(staff[i])
}var i = 0
while i < 10 {
print(i)
i += 1
}var i = 0
while i < 10 {
print(i)
// i += 1 commented out
}struct Student {
let age: Int
let classes: [String]
let major: String
let name: String
}let vin = Student(age: 19, classes: ["CS 1998", "PHYS 2213"], major: "Info Sci", name: "Vin Bui"){
"age": 19,
"classes": [
"CS 1998",
"PHYS 2213"
],
"major": "Info Sci",
"name": "Vin Bui"
}import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
}
}
// iOS 16 and below
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}#Preview {
ContentView()
}import OSLog
extension Logger {
/// The logger's subsystem.
private static var subsystem = Bundle.main.bundleIdentifier!
// NOTE: Replace the categories below with your own choosing.
/// All logs related to data such as decoding error, parsing issues, etc.
static let data = Logger(subsystem: subsystem, category: "data")
/// All logs related to services such as network calls, location, etc.
static let services = Logger(subsystem: subsystem, category: "services")
/// All logs related to tracking and analytics.
static let statistics = Logger(subsystem: subsystem, category: "statistics")
}Logger.services.info("Location Restricted")let birthday = "February 6, 2004"
Logger.data.info("User's birthday is \(birthday, privacy: .private)")func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// 1. Capture the scene
guard let windowScene = (scene as? UIWindowScene) else { return }
// 2. Create a new UIWindow and pass in a UIWindowScene
let window = UIWindow(windowScene: windowScene)
// 3. Create a view hierarchy programmatically
let rootVC = ViewController()
let navController = UINavigationController(rootViewController: rootVC)
// 4. Set the navigation controller as the window's root view controller
window.rootViewController = navController
// 5. Set the window and call makeKeyAndVisible()
self.window = window
window.makeKeyAndVisible()
}// Push
navigationController?.pushViewController(_ viewController: UIViewController, animated: Bool)
// Pop
navigationController?.popViewController(animated: Bool)let profileVC = ProfileViewController()
navigationController?.pushViewController(profileVC, animated: true)navigationController?.popViewController(animated: true)// Presenting
present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?)
// Dismissing
dismiss(animated: Bool, completion: (() -> Void)?)let profileVC = ProfileViewController()
present(profileVC, animated: true)dismiss(animated: true)func myName(name: String) {
print("My name is \(name)")
}myName(name: "Vin")func myName(name str: String) {
print("My name is \(str)")
}
myName(name: "Vin")func myName(_ name: String) {
print("My name is \(name)")
}
myName("Vin")func isEven(num: Int) -> Bool {
if num % 2 == 0 {
return true
}
return false
}HStack {
Text("Cornell")
Text("AppDev")
}VStack {
Text("Cornell")
Text("AppDev")
}ZStack {
Color.red
Text("AppDev")
}HStack {
Text("Cornell")
Spacer()
Text("AppDev")
}import XCTest
@testable import Sample
/// Test suite for Sample.
final class SampleTests: XCTestCase {
// Test procedures go here.
}func testCountEvens() {
var result: Int
// Test empty array
result = countEvens([])
XCTAssertEqual(result, 0)
// Test array with one even number
result = countEvens([2])
XCTAssertEqual(result, 1)
// Test array with more than one even number
result = countEvens([2,2])
XCTAssertEqual(result, 2)
// Test array with multiple even and odd numbers
result = countEvens([1,2,3,4,5,6,6])
XCTAssertEqual(result, 4)
}MapKit integration with WeatherAPI
Create a post and upload images
Like and delete a post
Lucy Yang, Kyle Chu, Nicole Qiu, Nathan Chu, Mihili Herath
A scheduling app that allows Cornell students to connect with coffee chatters and arrange coffee chats from a range of campus organizations.
Simple yet effective user interface
Easy on the eyes, not too much information at once
Consistent design system - typography, colors, etc.
Lots of explorations on Figma -> seems like everything was thought through pretty well
Ilyssa Yan, Claire Wang, Cassidy Xu, Ronald Leung, Andrew Qian, Emily Silkina
ShelterSwipe is an application where you can swipe through pets available for adoption at local shelters. We hope to match every potential pet-owner with their perfect animal to foster loving relationships and decrease the number of shelter animals.
Very creative, cute, and wholesome idea
Have never seen a swipe gesture used in a Hack Challenge before
Animations when swiping was pretty sick
Aidan Talreja, Peter Bidoshi, Daniel Chuang, Daniel Lee, Satya Datla
AI news platform that determines the political meaning of the news article based on AI and user ratings.
Summary generated with AI using NLP (natural language processing)
Clean and slick/simple UI
Can read articles within the app using WKWebView
User ratings + ML generated ratings
Share articles
Fall 2023 | Richie Sun
In Swift, there are two ways that we can build complex data types beyond the given basic types like Int, Float, and String. Classes are one such way that we can build some of these complex data types that we'll see later on in the UIKit framework as well as many other Swift packages.
If you have taken CS 1110 or 2110, then classes may already be familiar to you, but essentially; Classes can be thought of as blueprints. Within these blueprints, there are many different specifications and properties, for what we want the objects of the class to possess.
Thus, under the same analogy, objects are the houses that are built from the class blueprints
Class = Blueprint
Object = House built from blueprint
For example, let's suppose we define the following class for a house below:
Within the house class, there are many properties such as color, material, and owner along with their specified type, as well as any methods associated with the class.
However, with properties alone, the class is not complete, we need to also define an initializer as shown above. Essentially what the initializer does, is that it instantiates an object of the class.
In the code chunk above, notice the keyword self. The self keyword is used to represent an instance (or object) of the given class. In this case, the initializer creates the instance of the House class, which is then represented by the self keyword. Then, the properties color, material, and owner are initialized through self.
As shown below, the initializer is a function that is the same name as that of the Class, where we are able to pass in the desired values for the specified properties.
blueHouse and redHouse are both objects (or instances) of the same House Class
Now that we have an instance of that class we can then access its properties and call any class methods as shown below:
Classes can also be built based on other classes, this is known as class inheritance. This is a prominent technique that we'll see used extensively throughout UIKit, even in the most basic apps, so it's something we will need to eventually get familiar with.
Lets move back to our scenario with the House class that has properties color, material, and owner, and methods like the initializer and paintHouse method.
Supposed we wanted to define a new class to represent a TreeHouse, which includes all the properties that the House object does, but also includes new properties like treeType and slideColor.
Of course, it may seem logical at first to take all the code defined in the House class and copy it over to the TreeHouse, but this repetition of code may come back to bite us later on when we want to change the House class and also want the same changes to TreeHouse. We would have to change the same code twice!
Luckily, Swift offers a smarter solution with class inheritance: We can define the TreeHouse class based on our existing House class
The colon above is what establishes this inheritance, it indicates that TreeHouse is a subclass of House, or House is the superclass of TreeHouse; thus, TreeHouse will inherit all properties and methods from the House class.
However, we are not quite there yet, we also want to add the properties treeType and slideColor, and also change the paintHouse method so that we also have a color option to paint the slide.
Notice that we did not redefine any of the properties that already exist in House since they are inherited. In the initializer, notice a new keyword super. The keyword super represents the superclass, where in this case we are calling the initializer from the superclass House, which initializes the original 3 properties.
Now let us change the paintHouse function:
Notice the keyword override. In Swift, override indicates that a method is implemented in the superclass, but we want to change it for the subclass. Thus, if we want to redefine the paintHouse function to apply for TreeHouse, we need to “override” the existing method
Fall 2023 | Vin Bui
We have seen the four basic math operations in elementary school: addition, subtraction, multiplication, and division. In Swift, we can use operators to perform these operations.
The following lines are equivalent:
These operators are self-explanatory; however, if we were to take a closer look at the the line a = a / 10 we can notice that the output is 2 instead of 2.5. The reason for this is because the type of a is an Int. If we were to perform these operations on a, then we must also use an Int.
Then, how do we get the value 2.5? Because the type of a is an Int, then we must introduce a new variable of type Double or Float since we cannot change the type of a variable once initialized. We would also need to make sure that the values in which we apply these operators on must also be a Double or Float.
Let's take a look at the line Double(a). This is known as type casting. Because a is an Int and we needed a Double, Double(a) converts the value 2 to 2.0. Note that this does not change the type of a. It only produces a value to be used for that operation.
One more common operator we may see is the modulus operator (%). This is similar to the / operator except we return the remainder.
The following is a list of common operators that we are likely to use.
String interpolation is a way of combining variables and constants inside a string. Take a look at this example:
Of course, we could have used the + operator to concatenate these strings together.
The problem with this approach is efficiency especially if we want to concatenate multiple variables. Another issue with using + is that Swift does not allow types such as Int or Float to be glued with a String.
We could cast age to a String but that would be expensive.
Using string interpolation is a lot more efficient and looks cleaner too!
Fall 2023 | Reade Plunkett
AppIntents is the framework developed by Apple that allows us to configure our widgets with custom information provided by the user.
We will create a new intent that allows the user to input a location and have the weather widget update its displayed conditions.
Let’s start by creating a new Swift file, which we will call LocationAppIntent.
Within that file, we will define a new structure that conforms to the WidgetConfigurationIntent protocol. This protocol provides us with an interface for configuring a WidgetKit widget.
This protocol requires us to implement a title for this Intent. Additionally, we can also implement a description on what this intent does.
Next, we can define a parameter for this intent with a default value. The parameter will contain the location that use inputs.
Since our intent can make changes to a widget entry within our timeline, we must include it inside of the WeatherEntry model.
We must also update our timeline provider to support this new app intent. We will change our provider to conform to the AppIntentTimelineProvider protocol to do so.
Additionally, we will need to update the function headers for fetching a snapshot and timeline entry.
These two functions provide us with the intent called configuration that contain user-customized values for the location.
Within both of these functions, we have to update our WeatherEntry instantiation to pass in the configuration.
Additionally, we also have to update the entry created within our placeholder function with a new intent object.
Within WeatherWidget.swift, we also have to change our widget’s configuration from StaticConfiguration to AppIntentConfiguration.
Finally, within WeatherWidgetView, we can update the “Ithaca, NY” string to display the location passed in through the app intent associated with the timeline entry being displayed.
Additionally, to view the widget in the Preview window, we will need to provide it with a default configuration. We can define a default intent as an extension of our LocationAppIntent within our view. Since we do not want this default value being used anywhere outside this file, we mark it with fileprivate.
We then update the preview entries to use this new configuration.
Fall 2023 | Vin Bui
In this section, we will be using Alamofire and callbacks to perform network requests, specifically GET requests.
It is common in iOS development to create a class that represents our backend calls. Let’s create this class and name it NetworkManager. Make sure to import Alamofire.
We create this static variable called shared that holds a singleton and make the initializer private. This guarantees that only one instance of this class is created. To access this variable, we can simply use NetworkManager.shared
Inside of this NetworkManager class, we write functions to be called by our application. These functions will use Alamofire to send HTTP requests.
Assume there is a struct called Member that represents an AppDev member.
We can write the following code to fetch the AppDev roster.
Let’s break down this code.
Create a function called fetchRoster that takes in a callback (completion handler) as an argument. This callback takes in an array of Member objects.
Specify the endpoint which is the URL that we will call to fetch the data. We can test this with Postman.
Create a decoder to decode the data. If our object contains a Date property, then we need to specify the date decoding strategy. If the JSON contains fields with snake_case, then we also need to specify the key decoding strategy.
There is a problem with this code that will be discussed in the next section below.
We call the function that we just created inside of the NetworkManager class. Remember to use NetworkManager.shared. To access the value passed into the callback, we simply use the in keyword. For example, earlier we passed in an array of Member objects to the callback. In this case, fetchedMembers is holding the value that was passed in. We can call the variable whatever we want but it is very helpful to make it representative of the data.
Then, we do whatever we want with the data that we just fetched. If we are referencing a property outside of this function call, we will need to use self.
Next, if we need to perform any UI updates such as updating a collection view, we put that in a DispatchQueue.main.async block. This runs any code inside of the block on the main queue asynchronously. This is an advanced topic but the reason for this is so that the UI does not freeze while we are sending a network request.
weak selfAlthough the above code works, there is one major issue: we are retaining a strong reference to self inside of a closure which can cause retain cycles (causing memory leaks). To go around this, we will retain a weak reference to self inside of the closure and then unwrap a strong reference. We do this by adding [weak self] in our closure and using a guard let to unwrap a strong reference.
You may have noticed that we also use weak when creating our delegate properties. This is an advanced topic that we will discuss later, but for now, make sure to use weak self every time we need to use self in our networking calls.
Fall 2023 | Vin Bui
In the previous sections, we used Alamofire to send network requests. Although Alamofire is very elegant and simple to use, there are times when we want to use native Swift to send network requests. We can do this with URLSession.
Let’s take a look at the Alamofire version from the previous section:
We can do the same exact thing using URLSession (only steps after Step 3 are different):
Spring 2024 | Vin Bui
When configuring our workflow, we select a branch to indicate where Xcode Cloud will listen for changes. Depending on how we configured it, a commit or merge request will trigger Xcode Cloud to begin making the build.
The process begins by creating the environment in which our repository gets cloned. After the repository is cloned, a Post Clone Script that we create is used to install dependencies such as CocoaPods as well as download any secrets to the directory. After this step, build actions are ran. We can also create a Pre Build Script as well as a Post Build Script. Finally, post-build actions are ran such as notifying to a Slack channel, pushing to TestFlight, etc.
In addition to configuring our flow to just archiving, we can set it to do other things such as running UI or Unit Tests.
var a = 0
a = a + 10
a = a - 5
a = a * a
a = a / 10import Alamofire
class NetworkManager {
/// Shared singleton instance
static let shared = NetworkManager()
// Prevent other instances from being created
private init() { }
}==
equal to
!=
not equal to
>
greater than
||
or
>=
greater than or equal to
&&
and
<
less than
!
not
<=










less than or equal to
Create the request using Alamofire.
Pass in the endpoint and specify the method (.get for GET, .post for POST, etc).
Validate the request to ensure that the status code is 2xx and if the content type matches.
Decode the response to [Member] using the decoder we created in Step 3.
Perform a switch statement on the response’s result.
If successful, pass to the callback the decoded response. A print statement is optional but recommended.
If failed, print an error statement about the error.
Let’s take a look at a POST request using Alamofire:
If we were to use URLSession, we need to:
Change the httpMethod property of the URLRequest to "POST".
Call the setValue function with ("application/json", forHTTPHeaderField: "Content-Type").
Set the httpBody property of the URLRequest.
The code below uses JSONSerialization to serialize the dictionary parameters to JSON and use it as the request body.
Alternatively, we can also use a JSONEncoder to encode our object into JSON, but it won’t be shown here.
class House {
var color: String
var material: String
var owner: String
init(color: String, material: String, owner: String) {
self.color = color
self.material = material
self.owner = owner
}
func paintHouse(color: String) {
self.color = color
}
}let blueHouse = House(color: "blue", material: "brick", owner: "Vin")
let redHouse = House(color: "red", material: "wood", owner: "Richie")let house = House(color: "white", material: "wood", owner: "Reade")
house.color // white
house.paintHouse(color: "blue")
house.color // blueclass House {
var color: String
var material: String
var owner: String
init(color: String, material: String, owner: String) {
self.color = color
self.material = material
self.owner = owner
}
func paintHouse(color: String) {
self.color = color
}
}class TreeHouse: House {
// Class implementation here
}let treeHouse = TreeHouse(color: "red", material: "wood", owner: "Tiffany")
treeHouse.color // red
treeHouse.paintHouse(color: "blue")
treeHouse.color // blueclass TreeHouse: House {
var treeType: String
var slideColor: String
init(color: String, material: String, owner: String, treeType: String, slideColor: String) {
self.treeType = treeType
self.slideColor = slideColor
super.init(color: color, material: material, owner: owner)
}
}override func paintHouse(color: String, slideColor: String) {
self.color = color
self.slideColor = slideColor
}var name = "Vin"
"My name is \(name)."import WidgetKit
import AppIntents
struct LocationAppIntent: WidgetConfigurationIntent {
}static var title: LocalizedStringResource = "Location"
static var description = IntentDescription("Enter a location to view the weather.")@Parameter(title: "Location", default: "Ithaca, NY")
var location: Stringimport WidgetKit
struct WeatherEntry: TimelineEntry {
let date: Date
let weather: Weather
let configuration: LocationAppIntent
}struct Provider: AppIntentTimelineProvider {
}func snapshot(for configuration: LocationAppIntent, in context: Context) async -> WeatherEntry {
}func timeline(for configuration: LocationAppIntent, in context: Context) async -> Timeline<WeatherEntry> {
}let entry = WeatherEntry(date: Date(), weather: possibleWeathers.randomElement()!, configuration: configuration) func placeholder(in context: Context) -> WeatherEntry {
return WeatherEntry(date: Date(), weather: .sunny, configuration: LocationAppIntent())
}AppIntentConfiguration(kind: kind, intent: LocationAppIntent.self, provider: Provider()) { entry in
WeatherWidgetView(entry: entry)
}HStack {
Text(entry.configuration.location)
.font(.system(size: 18, weight: .semibold))
.minimumScaleFactor(0.5)
Spacer()
}extension LocationAppIntent {
fileprivate static var cupertino: LocationAppIntent {
let intent = LocationAppIntent()
intent.location = "Cupertino, CA"
return intent
}
}#Preview(as: .systemSmall) {
WeatherWidget()
} timeline: {
WeatherEntry(date: .now, weather: .sunny, configuration: .cupertino)
WeatherEntry(date: .now, weather: .cloudy, configuration: .cupertino)
WeatherEntry(date: .now, weather: .overcast, configuration: .cupertino)
WeatherEntry(date: .now, weather: .rainy, configuration: .cupertino)
WeatherEntry(date: .now, weather: .lightning, configuration: .cupertino)
WeatherEntry(date: .now, weather: .snowy, configuration: .cupertino)
}struct Member: Codable {
let name: String
let subteam: String
let position: String
}// 1. Create the function
func fetchRoster(completion: @escaping ([Member]) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 4. Create the request
AF.request(endpoint, method: .get)
.validate()
.responseDecodable(of: [Member].self, decoder: decoder) { response in
// 5. Handle the response
switch response.result {
case .success(let members):
print("Successfully fetched \(members.count) members")
completion(members)
case .failure(let error):
print("Error in NetworkManager.fetchRoster: \(error)")
}
}
}NetworkManager.shared.fetchRoster { fetchedMembers in
// Do something with the data such as...
self.members = fetchedMembers
DispatchQueue.main.async {
// Perform UI updates such as...
self.collectionView.reloadData()
}
}NetworkManager.shared.fetchRoster { [weak self] fetchedMembers in
// Unwrap a strong reference
guard let self = self else { return }
// Do something with the data such as...
self.members = fetchedMembers
DispatchQueue.main.async {
// Perform UI updates such as...
self.collectionView.reloadData()
}
}// 1. Create the function
func fetchRoster(completion: @escaping ([Member]) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 4. Create the request
AF.request(endpoint, method: .get)
.validate()
.responseDecodable(of: [Member].self, decoder: decoder) { response in
// 5. Handle the response
switch response.result {
case .success(let members):
print("Successfully fetched \(members.count) members")
completion(members)
case .failure(let error):
print("Error in NetworkManager.fetchRoster: \(error)")
}
}
}// 1. Create the function
func fetchRoster(completion: @escaping ([Member]) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 4. Create a URLRequest
var urlRequest = URLRequest(url: URL(string: endpoint)!)
urlRequest.httpMethod = "GET"
// 5. Create and resume the task
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
// 6. Handle the response (you can also do something with `response`)
guard error == nil, let data else {
print("Error in NetworkManager.fetchRoster: \(error)")
return
}
if let members = try? decoder.decode([Member].self, from: data) {
completion(members)
}
}
.resume()
}// 1. Create the function
func addToRoster(member: Member, completion: @escaping (Member) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Define the request body
let parameters: Parameters = [
"name": member.name,
"subteam": member.subteam,
"position": member.position
]
// 4. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 5. Create the request
AF.request(endpoint, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.validate()
.responseDecodable(of: Member.self, decoder: decoder) { response in
// 5. Handle the response
switch response.result {
case .success(let member):
print("Successfully added member \(member.name)")
completion(member)
case .failure(let error):
print("Error in NetworkManager.addToRoster: \(error.localizedDescription)")
}
}
}// 1. Create the function
func addToRoster(member: Member, completion: @escaping (Member) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Define the request body
let parameters = [
"name": member.name,
"subteam": member.subteam,
"position": member.position
]
guard let httpBody = try? JSONSerialization.data(
withJSONObject: parameters,
options: []
) else { return }
// 4. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 5. Create a URLRequest
var urlRequest = URLRequest(url: URL(string: endpoint)!)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = httpBody
// 6. Create and resume the task
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
// 7. Handle the response (you can also do something with `response`)
guard error == nil, let data else {
print("Error in NetworkManager.addToRoster: \\(error)")
return
}
if let member = try? decoder.decode(Member.self, from: data) {
completion(member)
}
}
.resume()
}If our app requires dependencies, we will first need to create a custom build script to be executed.
Create a branch called release in the GitHub repository.
Inside of the workspace, create a Group called ci_scripts. This folder must be located in the root directory.
There are three scripts possible scripts to make in here. See this link. Note that we may not need all of them.
Make sure to make these scripts executable by running chmod +x <file_name.sh>.
If our app contains secrets, we will most likely have to use environment variables for security reasons since these scripts must be published in the GitHub repository.
may also be helpful.
Push all of these changes to the release branch.
In Xcode, go to Integrate > Xcode Cloud > Create Workflow.
Select the Product.
Select Edit Workflow.
Under General, make sure that the correct Primary Repository is selected. Change the Name to Release and give it a description such as “Shipping new updates to the AppStore.”
Under Environment, select Clean.
Add any environment variables used by our scripts. Make sure to check Secret if they are secretive.
Under Branch Changes, remove master and add release. We want to use master for development only and then merge the master branch to release when ready for production.
Under Archive - iOS choose iOS and make sure the proper scheme is selected (e.g. a production environment scheme)
Check App Store.
Click Save and enable Xcode cloud to have access to the GitHub repository.
This will redirect us to GitHub. Make sure to choose Only select repositories and select the correct repository.
Go back to Xcode and make sure a green checkmark appears indicating that Xcode cloud has access, then click Next > Complete.
Create a first build for the release branch. Note that this will default to Build 1, but we can change this later.
We can view all builds under the Xcode Cloud tab in AppStore Connect.
If there is already a build version, select the next build number in settings.
Under Post Actions we can configure it to notify Slack channels, upload to TestFlight, etc.
It’s often the standard that we follow this approach when developing.
Create a branch off of master and work on that branch.
Create a PR to merge the branch to master or main. Note that these changes are only meant for development.
When we are ready to release changes to the AppStore, we can then merge master or main to release.
In the case that there are merge conflicts, create a branch off of release (call it release-copy) and merge our master or main branch to resolve merge conflicts. Make sure to change the app version as well as the build version (even though the build version should automatically be incremented by Xcode Cloud). When done, submit a PR to merge release-copy to release.
Now, there are times when we want to make changes to the AppStore version without interfering with the development side.
In this case, you want to create a working branch off of release.
Then, increment the build version as well as the app version.
Submit a PR to merge that working branch to release. If there are merge conflicts, follow the same process mentioned above.




Fall 2023 | Vin Bui
Inside of our NetworkManager class, we write functions to be called by our application. These functions will use Alamofire to send HTTP requests.
Assume there is a struct called Member that represents an AppDev member.
We can write the following code to add a member to the AppDev roster store in the backend.
Let’s break down this code.
Create a function called fetchRoster that takes in a callback (completion handler) as an argument. This callback takes in a Member object. Note that this depends on what this POST request returns from the backend. However, we can expect the backend to return the member that we just added back to us.
Specify the endpoint which is the URL that we will call to fetch the data. We can test this with Postman.
Define the request body. This POST request takes in three fields inside of the request body. Using the member
Let’s point out the differences between this and a GET request.
The function header usually contains a parameter storing some value that we want to send to the backend. In this case, we had a parameter called member storing a Member object.
We defined a dictionary whose type is Parameters. Remember, these are key-value pairs.
Our method is .post
Or if we plan on using self somewhere,
This is exactly the same as it was with a GET request except that we have to pass in an argument to the function we created.
In the code samples above, our callback took in a Member object. Sometimes the backend does not return this information, or we may not even need this information. Instead, it may be more useful for the callback to hold a more useful variable type: a Bool.
If we were to refactor our code, it would look something like this:
The main difference here is that the callback takes in a Bool instead of the Member object. If the response succeeds, we use completion(true) to pass in true to the callback. If the response fails, we use completion(false).
When we call this function, we use success to access the value passed to the callback. We can then do whatever we need depending on the status of the network request.
Fall 2023 | Vin Bui
The main purpose of the delegate pattern is to allow a child to communicate back with its parent without the child knowing its parent’s type. This makes it much easier to maintain and write reusable code. Delegation is a 1:1 relationship with a child and its parent.
To implement delegation in Swift, we use protocols. According to Swift’s official documentation,
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.
In other words, protocols are a set of properties and methods that classes must implement when conforming to it (similar to interfaces in Java).
You can learn more about protocols , but for our purpose, we will only need to define functions inside of the protocol. For example, If we wanted to create a protocol so that a child can tell its parent to update some text, we could do the following:
Make sure to conform the protocol to AnyObject!
It’s convention to name our protocol with its purpose followed by the word Delegate at the end. We also created a function called updateText that takes in a string called newText. Now, if we had a class called ParentViewController and we wanted to conform to this protocol, we could do the following:
Now, it’s common to use an extension to implement these functions to keep our code a lot more neat:
The only changes that we made was removing UpdateTextDelegate from the original class header and moved it to the extension header, followed by the function implementation required by the protocol.
In the code above, we conformed ParentViewController to the UpdateTextDelegate protocol. The class ParentViewController is known as the delegate. The delegate is the class that conforms to that protocol.
Now, if we had a view controller called ChildViewController that wants to communicate back with the parent, then this view controller is known as the delegator. The delegator is the class that wants the delegate to do something by calling the delegate.
We’ve looked at the code required by the delegate, but what about the delegator? Inside of ChildViewController, we would create a property whose type will be UpdateTextDelegate:
How does this child class know who the delegate is? The parent class would need to specify that it itself is the delegate:
Now, once the child has a reference to its parent (specifically, a weak reference, will explain this in another chapter), we can now call that function:
Note: In the example code above, we created the functions pushChildVC and communicateBack. These functions could be anything. What’s important is the code inside of the function.
There was a lot of code and it could be quite difficult to wrap our head around, so let's put everything together.
There are two classes: ParentViewController (delegate) and ChildViewController (delegator). ParentViewController is the delegate so it conforms to the protocol, meaning that it is required to implement the functions and properties defined by that protocol. When ParentViewController creates the ChildViewController, it will need to tell it that it itself is the delegate. We do this by creating a property in ChildViewController containing the reference of the delegate (which is ParentViewController). To communicate from the child to the parent, the child calls the function defined in the protocol using the delegate property that was created earlier. This child could then pass in whatever it wants to the function (since it’s the delegator) and the parent (the delegate) will use whatever the child passed in and do whatever it needs to do.
Let’s take a real life example to understand this better. Say we went to a bar to have a couple of drinks. The bar contains a menu, a bartender, and a customer. The menu is the protocol, the bartender is the delegate, and the customer is the delegator. The bartender must conform to the menu. In other words, the bartender can only make drinks that are on the menu. But how does the bartender know what drinks to make? Well, the customer (delegator), tells the bartender (delegate) what drinks to make. In other words, the customer cannot make the drinks themself but requires the bartender to do it for them.
Fall 2023 | Vin Bui
Sometimes we may want to show that our data does not have any value. If we were using Strings, then an empty string may be a good indicator for “no value”. What about integers? We could use 0 or -1. The problem with this is that we are creating imaginary rules for ourselves. Swift solves this issue by introducing optionals.
Spring 2024 | Vin Bui
Google Analytics is an app measurement solution that allows us to view our app’s usage and other insights. It allows us to understand how people use our applications through defining and tracking custom user events. It helps us understand the behavior of our users, allowing us to make informed product decisions and improving the user experience.
In order to set up Google Analytics, we must have a Firebase project configured for our app.
struct Member: Codable {
let name: String
let subteam: String
let position: String
}// 1. Create the function
func addToRoster(member: Member, completion: @escaping (Member) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Define the request body
let parameters: Parameters = [
"name": member.name,
"subteam": member.subteam,
"position": member.position
]
// 4. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 5. Create the request
AF.request(endpoint, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.validate()
.responseDecodable(of: Member.self, decoder: decoder) { response in
// 5. Handle the response
switch response.result {
case .success(let member):
print("Successfully added member \(member.name)")
completion(member)
case .failure(let error):
print("Error in NetworkManager.addToRoster: \(error)")
}
}
}MembermemberCreate a decoder to decode the data. If our object contains a Date property, then we need to specify the date decoding strategy. If the JSON contains fields with snake_case, then we also need to specify the key decoding strategy.
Create the request using Alamofire.
Pass in the endpoint and specify the method (.get for GET, .post for POST, etc).
Encode the request with JSON to be sent over the internet (POST).
Validate the request to ensure that the status code is 2xx and if the content type matches.
Decode the response to Member using the decoder we created in Step 4.
Perform a switch statement on the response’s result.
If successful, pass to the callback the decoded response. A print statement is optional but recommended.
If failed, print an error statement about the error.
.getJSONEncoding.defaultWe pass in parameters to the AF.request function. In the GET request version, we did not do this.
NetworkManager.shared.addToRoster(member: someMemberVariable) { fetchedMember in
// Do something with the data
DispatchQueue.main.async {
// Perform UI updates
}
}NetworkManager.shared.addToRoster(member: someMemberVariable) { [weak self] fetchedMember in
// Unwrap a strong reference
guard let self = self else { return }
// Do something with the data
DispatchQueue.main.async {
// Perform UI updates
}
}// 1. Create the function
func addToRoster(member: Member, completion: @escaping (Bool) -> Void) {
// 2. Specify the endpoint
let endpoint = "<Enter URL String Here>"
// 3. Define the request body
let parameters: Parameters = [
"name": member.name,
"subteam": member.subteam,
"position": member.position
]
// 4. Create a decoder
let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601 // Only if needed
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
// 5. Create the request
AF.request(endpoint, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.validate()
.responseDecodable(of: Member.self, decoder: decoder ) { response in
// 5. Handle the response
switch response.result {
case .success(let member):
print("Successfully added member \(member.name)")
completion(true)
case .failure(let error):
print("Error in NetworkManager.addToRoster: \(error)")
completion(false)
}
}
}NetworkManager.shared.addToRoster(member: someMemberVariable) { success in
if success {
// Do something if the call succeeded
} else {
// Do something if the call failed
}
}protocol SomeProtocol: AnyObject {
// Protocol definition here
}protocol UpdateTextDelegate: AnyObject {
func updateText(newText: String)
}// Include `UpdateTextDelegate` in the class header
class ParentViewController: UIViewController, UpdateTextDelegate {
// Class definition here
// Must implement the function `updateText`
func updateText(newText: String) {
// Update some text given `newText`
}
}// Remove `UpdateTextDelegate` from the class header
class ParentViewController: UIViewController {
// Class definition here
}
// Add `UpdateTextDelegate`
extension ParentViewController: UpdateTextDelegate {
func updateText(newText: String) {
// Update some text given `newText`
}
}class ChildViewController: UIViewController {
// Class definition here
// Create the property
// If we make this private, an initializer is required
weak var updateTextDelegate: UpdateTextDelegate?
}// Assume this class conforms to `UpdateTextDelegate`
class ParentViewController: UIViewController {
// Class definition here
// When we create an instance to `ChildViewController`, pass in
// `self` as the delegate
private func pushChildVC() {
let childVC = ChildViewController()
childVC.updateTextDelegate = self
navigationController?.pushViewController(childVC, animated: true)
// If the property `updateTextDelegate` is private, we will need
// an initializer in `ChildViewController` that initializes it
// and use this line instead
let childVC = ChildViewController(updateTextDelegate: self)
}
}class ChildViewController: UIViewController {
// Class definition here
// Create the property
// If we make this private, an initializer is required
weak var updateTextDelegate: UpdateTextDelegate?
// Communicate with parent
private func communicateBack() {
let newString = "Here you go parent"
updateTextDelegate.updateText(newText: newString)
}
}Developer Skills
How to implement functions according to a specification
How to read documentation from outside resources
How to read, create, and use functions to organize code
How to work with Git and GitHub for version control
Course Material
How to use string interpolation to combine variables with strings
How to convert data types using type casting
How to create and work with arrays and dictionaries
How to use conditionals to control program flow
How to use methods provided by Swift
How to use loops to repeat code
How to work with optionals
How to use higher order functions to simplify code
As with any other course at Cornell, the Code of Academic Integrity will be enforced in this class. All University-standard Academic Integrity guidelines should be followed. This includes proper attribution of any resources found online, including anything that may be open-sourced by AppDev. The University guidelines for Academic Integrity can be found here.
This assignment must be done individually. However, we do encourage limited collaboration. You are also free to come to the instructors or any course staff for help. Programming forums like Stack Overflow or Hacking with Swift are allowed as long as you understand the code and are not copying it exactly. Code written through AI means such as ChatGPT is NOT ALLOWED. However, you may use these resources for assistance, although we highly encourage consulting Ed Discussion or office hours instead.
If you are stuck or need a bit of guidance, please make a post on Ed Discussion or visit office hours. Please do not publicly post your code on Ed Discussion. If you are using an external resource such as Stack Overflow, keep in mind that we are using UIKit with Swift 5. If you see anything with @IBOutlet or any weird syntax, then you are most likely looking at a different version.
The grading for TODOs 1-9 are based on the number of test cases that you pass. We will convert the values to a decimal and their sum will be your subtotal (out of 10). The feedback form link is located in the Submission section of this handout.
TODO 1: introduce
_ / 2
TODO 2: getStudentInfo
_ / 1
TODO 3: countEvens
_ / 4
TODO 4: capitalizeStrings
_ / 4
TODO 5: repeatString
_ / 4
TODO 6: countWords
_ / 6
Download and unzip the files at the top of this page. Navigate to the files located on your local computer drive. Inside of the folder should contain an Xcode project called A1.xcodeproj. Open up the project.
Once you have the project opened, on the left side of the screen you should see the Navigator which contains all of the folders and files in the directory. If not, press CMD + 0 (that’s a zero) on your keyboard.
If you expand everything underneath A1 you should see the following:
You will be working on MainApp.swift and A1Tests.swift.
You will be implementing the functions provided in this file. There are a total of 9 TODOs. If you click on this red box at the top of your Xcode, there should be a dropdown menu.
If you click on the clipboards, you will directed to the TODOs. The stars (⭐️) represent the difficulty level of each function. At the top of each function header is the specification. Your goal is to implement the function according to the specification. DO NOT CHANGE THE FUNCTION HEADER. We have given you hints to help you complete the tasks.
This file contains the test cases for each function. DO NOT EDIT THIS FILE.
There are two ways to run the test cases:
You can run the entire test suite by clicking on the button in the blue box next to final class A1Tests: XCTestCase in the Editor or A1Tests in the Navigator on the left.
You can run test cases for a specific function by clicking on the button in the blue box next to the function (such as func testIntroduce()) in the Editor or in the Navigator on the left.
When you run the test suite for the first time, it may take about 30 seconds to 1 minute to load. After the first launch, it should not take that long. If the Simulator opens up, keep it open as it is required to run the test suite (for some reason). You will get a popup saying “Build Succeeded”, but this does not mean that you have passed the test cases. A passed test case will have green checkmarks and no error messages.
The yellow box above indicates an error message. The value pointed by the pink arrow is the “Received” output which is what your implementation returned. The value pointed by the green arrow is the “Expected” output which is what your function should return. The console will also output the error message.
Make sure you are using an iPhone simulator at the top of Xcode.
There are a total of 9 functions that you need to implement with varying levels of difficulty (indicated by a ⭐️). Follow these steps when working on the assignment:
Begin TODO 1 and implement the function.
Run the test function for TODO 1. If failed, fix your function and try again. If passed, move on to the next step.
Repeat for TODOs 2-9
Double-check that all of your files are properly filled out.
Zip all of your files, you can right-click the folder they are in and click "compress"
Submit the assignment to CMSX
Fill out this feedback survey (worth 1 point).
To indicate an optional in Swift, we use a ? succeeding the data type. For example, a string optional (or optional string) is represented by String?. This String optional can hold two things:
a String value
nil
nil means “nothing” or “no value”. To better understand optionals, let’s look at the following example:
This function returns a String optional with value “iOS is the best subteam” if the argument is "ios" and nil otherwise. Let’s put this in the playground and try to store this value into a variable:
What is the issue with this code? Well, the type of the variable iosLead is a String but the function returns a String?. These two data types are different. In that case, we could change the data type of iosLead to String?.
Okay, but what if there was a function that only takes in a String and not a String? but we still want to use the value returned from getSubteamLead?
This code will not execute because getSubteamLead returns a String? but the function cheerLead takes in a String. In this case, we would need to unwrap the optional.
In order to grab the non-nil value of an optional, we must unwrap it. There are three ways to do this:
if let
guard let
Force unwrapping (!)
The first two provides a safe way to unwrap the optional. Using the example from earlier, let’s try to unwrap the optional:
The constant leadName holds the unwrapped value returned from the function call getSubteamLead. We would then use leadName within the if statement. Now, if the function returned nil instead, then the block of code will not be executed.
We could also use a guard let statement:
The main difference between using an if let versus a guard let statement is the scope of the variable/constant. The constant leadName lives within the block of code in an if let statement whereas in a guard let statement, it lives outside of it. As we can see above, we are able to use the constant leadName outside of the guard let statement.
Another (not recommended) approach to unwrap an optional is to force unwrap it using an exclamation mark (!).
Be careful! If we try to unwrap an optional that is holding nil, our program will crash!
Let me emphasize this again. Our code will crash if we unwrap an optional that is holding nil. We should only use this approach if we are 100% certain that the optional holds an actual value. However, most of the time we should not have to use this. Let’s use the code from earlier:
In this case, we know that the code will not crash because we are certain that leadName will not hold nil. However, leadName could hold nil and our code will crash if it does.
Earlier, we mentioned that we can indicate an optional by using a question mark (?). For example, we can indicate a String optional by doing String?. We can also use an exclamation mark (!) such as String!. The difference between these two is that the constant or variable with the data type that contains the exclamation mark, does not need to be unwrapped before it is used. This is called an implicitly unwrapped optional. We unwrap the optional the moment the variable or constant is initialized. We are very likely to see this when we get into UIKit.
It can get very annoying having to unwrap optionals using guard let or if let statements and can clutter our code a lot. This may cause many people to be tempted to force unwrap an optional which we should already know is not good. Let’s take a look at the following code:
If we put this code in the playground, Xcode will give us an error.
The problem is that the uppercased method is only available for String types, not String? types. Since getSubteamLead returns a String? we would need to unwrap it before we can use it in the uppercased method. However, this is very annoying to do and can make our code cluttered. Thankfully, Swift allows us to use optional chaining:
That extra ? after the call to getSubteamLead is the optional chaining. This means everything after the ? will only be run if everything before it has a value and is not nil. Try this in the playground and the error message will go away.
Another clean way to handle optionals in our code is to use the nil coalescing operator. The following code is an example of how to use it:
The ?? is the nil coalescing operator and it provides a default value if the optional is holding nil. In the code above, if the call getSubteamLead(subteam: "design") returned nil, then the constant designLead will hold the default value "Invalid" instead of nil. This is very nice because we do not have to unwrap anything and ensures that there is an actual value.
GoogleService-Info.plist file and drag it into your project directory.Install the Firebase SDK via CocoaPods or Swift Package Manager and choose the libraries that you want to use (in this case FirebaseAnalytics).
Follow the instructions from the Firebase website to add the initialization code.
The official Firebase documentation does a pretty good job explaining how to set up Google Analytics so I won’t repeat it here. Follow the guide to add the SDK.
Our first step is to create a file that will manage our analytics. Let’s call this file AnalyticsManager.swift.
We can create custom events that can be triggered as a response to user interactions.
This is a general Event type that will be used by Analytics to log. Let’s make it more specific to our app. Let’s use Uplift in our example.
Create an enum called UpliftEvent with a raw String type.
Define custom events with a case statement. The raw value should be separated by underscores. In this example, we created an event that tracks the tapping of a gym cell.
Create a nested enum called EventType. This enum will be used if we need to pass in parameters along with our events.
In the event we defined above, since we are tracking a user’s interaction with a gym cell, it would be useful to know which gym they are selecting. We can pass in parameters that will contain this information (which is why we create a gym case in our EventType enum).
Create a function that converts our UpliftEvent to the Event struct that we created earlier.
We handle the case in which there are no parameters using a guard let statement and return just the raw value of the enum.
If there are parameters, we can perform a switch
After defining all of our event types, we can then begin writing the actual AnalyticsManager.
Just like any other utils manager we create (such as NetworkManager), we create a shared singleton instance and make the initializer private.
We then create a single function that takes in an Event type.
The #if DEBUG statement is used to determine our app’s build setting. When working in a development environment, we do not want to be tracking analytics. Instead, we can print (or even better, log) the event in the development environment.
In our example, we can log the tapping of a gym cell with the following code:
Spring 2024 | Vin Bui
This chapter focuses on the important fundamentals that we must think about when creating a brand new project. As a disclaimer, there are various ways to create a project from scratch, but this is just how I prefer to do things.
The first thing that we must think about is the structure of our codebase. This often depends on the framework that we are using. For example, it’s more common to use MVC (Model-View-Controller) in UIKit and MVVM (Model-View-ViewModel) in SwiftUI. There are also other popular architectures such as TCA (The Composable Architecture) and VIPER.
How do we know which one to use? The answer is, it depends. Some architectures are more friendly when working with a large team such as VIPER which emphasizes more files with less code. It’s important that we choose the architecture that best suits our needs and our environment. Despite which pattern we choose, it’s more important that we stick with it and build on top of it.
Now that we have chosen an architecture pattern, we can now create folders and other subdirectories to structure our project. This is the structure that I like to use with MVVM and highly recommend:
Configs: the app environment settings, like production, staging, and development configurations, all using the .xcconfig files.
Core: the app’s entry point.
Models: model objects used by the API and throughout the app.
Next up, we must think about how to manage dependencies. Most of the time, we will be importing code written by others so it’s important that we decide which dependency manager to use. There are various managers we can use such as Swift Package Manager, CocoaPods, Carthage, etc.
I recommend using Swift Package Manager (SPM) since it is the newest of them all and is highly supported by Apple. It works seamlessly with Xcode and is very simple to use. If there are packages not available through Swift Package Manager, the next best alternative is CocoaPods. CocoaPods was a very popular choice before SPM was introduced, and it works with other operating systems such as MacOS as well! Note that we can mix SPM with other managers. Learn more about CocoaPods below.
In many applications, we often have multiple environments: a staging environment (also known as development) and a production environment. As a result, our iOS code may need to change depending on the environment. For example, if we are logging Analytics, we only want to log during a production environment.
To handle this logic, we typically use a #if DEBUG or #if RELEASE statement. How does this work? If we click on our scheme at the top of Xcode and look at Build Configuration, we can see that there are two options: Debug and Release.
The most common way to change between environments is to create two different schemes: one that uses a Debug configuration, and one that uses a Release configuration. This way, we can easily change the environment by simply changing the scheme.
Now that we have introduced debug and release configurations, let’s talk about secrets and environment variables. These are essentially values that are set outside of our program but are crucial in running our application. Additionally, these are commonly keys or files that contain sensitive information and should not be pushed into a public repository (should be on a .gitignore).
Let’s say we have a backend endpoint that we want to (and should) keep hidden from the public. We can include the value in a .plist or a .xcconfig file.
If using the xcconfig approach, we can create a file called Keys.xcconfig to store our environment variables. Note the usage of capital letters and underscores.
To use these variables, we must import them through our app’s Info.plist. In the example above, we can create a new key called DEV_URL with the type String and value $(DEV_URL). A dollar sign with parentheses is required and must match exactly with the value in the xcconfig file.
Finally, in Project > Info > Configurations, set the configuration to use our xcconfig file.
To access values from a property list (plist), let’s first create an enum to represent our app’s environment. We will use Uplift in this example.
The static computed property infoDict converts our Info.plist file to a dictionary. We can then access this value just like any dictionary in Swift (as seen in the baseURL computed property). Notice the usage of #if DEBUG to determine the value of Keys.baseURL.
If we used a custom property list instead of a config file, then Bundle.main.infoDictionary will not work. Use the following code instead:
Most of the time, our application’s user interface will be created by some designer. The design will typically follow some kind of design system which includes the app’s font choices, colors, images, etc. To have full control of our application, we typically create a constants file which contains constants used throughout our app.
Now, there are many ways to represent these constants. One approach would be to use extensions. For example, we can create the following Color extension:
This is a pretty good approach because we can simply use Color.volume.backgroundGray to access the color. However, this is not the ideal usage of extensions and requires multiple files to be created.
We can do better by creating a Constants.swift file inside of our Utils folder.
We create a struct called Constants which contains multiple caseless enums. Using caseless enums is preferred over structs because we do not have to worry about any object-oriented logic such as instantiation, methods, properties, etc. To use a color, we can simply do Constants.Colors.backgroundGray. Now, our app’s constants are all located in a single file, making it much easier to change if needed.
The final important fundamentals we need to think about are linting and logging, both of which involve making our code readable and easy to digest by other developers.
The most popular linting library for Swift is SwiftLint, which can be installed with almost any dependency manager. We won’t go over how to use SwiftLint since their documentation explains it pretty well, but with SwiftLint, we can define rules and even make exceptions for some lines.
It’s also very important that we have a system of logging our applications’ error messages, debug messages, warnings, etc. The recommended way to do this is by using OSLog which works seamlessly with the Xcode 15 console. Learn more about it here.
Below are a few other topics that we may also need to consider when creating our app’s foundation.
Image Caching (Nuke, KingFisher, SDWebImage, etc.)
Networking
Unit/UI Tests
CI/CD (fastlane, Jenkins, Xcode Cloud)
Fall 2023 | Vin Bui
Creating a UITableViewCell is very similar to how we have been creating views inside of a UIViewController. We still define properties for the view and data, initialize those views, add them as a subview, and constrain those views. However, when working inside of a UITableViewCell there are some slight modifications that we will need to make. Follow these steps:
func getSubteamLead(subteam: String) -> String? {
if subteam == "ios" { return "Tiffany Pan" }
if subteam == "design" { return "Christina Zeng" }
if subteam == "marketing" { return "Eddie Chi" }
if subteam == "android" { return "Emily Hu" }
if subteam == "backend" { return "Joyce Wu" }
return nil
}func getSubteamLead(subteam: String) -> String? {
if subteam == "ios" { return "Tiffany Pan" }
if subteam == "design" { return "Christina Zeng" }
if subteam == "marketing" { return "Eddie Chi" }
if subteam == "android" { return "Emily Hu" }
if subteam == "backend" { return "Joyce Wu" }
return nil
}
func cheerLead(name: String) {
print("Woo! Go \(name)!")
}
cheerLead(name: getSubteamLead(subteam: "ios"))func getSubteamLead(subteam: String) -> String? {
if subteam == "ios" { return "Tiffany Pan" }
if subteam == "design" { return "Christina Zeng" }
if subteam == "marketing" { return "Eddie Chi" }
if subteam == "android" { return "Emily Hu" }
if subteam == "backend" { return "Joyce Wu" }
return nil
}
func cheerLead(name: String) {
print("Woo! Go \(name)!")
}
if let leadName = getSubteamLead(subteam: "ios") {
cheerLead(name: leadName)
}func getSubteamLead(subteam: String) -> String? {
if subteam == "ios" { return "Tiffany Pan" }
if subteam == "design" { return "Christina Zeng" }
if subteam == "marketing" { return "Eddie Chi" }
if subteam == "android" { return "Emily Hu" }
if subteam == "backend" { return "Joyce Wu" }
return nil
}
func cheerLead(name: String) {
print("Woo! Go \(name)!")
}
// Create a custom error
enum myError: Error {
case incorrectSubteam
}
guard let leadName = getSubteamLead(subteam: "ios") else {
// Usually we would return something here but because this is not
// a function, we had to throw an error
throw myError.incorrectSubteam
}
cheerLead(name: leadName)func getSubteamLead(subteam: String) -> String? {
if subteam == "ios" { return "Tiffany Pan" }
if subteam == "design" { return "Christina Zeng" }
if subteam == "marketing" { return "Eddie Chi" }
if subteam == "android" { return "Emily Hu" }
if subteam == "backend" { return "Joyce Wu" }
return nil
}
func cheerLead(name: String) {
print("Woo! Go \(name)!")
}
let leadName = getSubteamLead(subteam: "ios")
cheerLead(name: leadName!)func getSubteamLead(subteam: String) -> String? {
if subteam == "ios" { return "Tiffany Pan" }
if subteam == "design" { return "Christina Zeng" }
if subteam == "marketing" { return "Eddie Chi" }
if subteam == "android" { return "Emily Hu" }
if subteam == "backend" { return "Joyce Wu" }
return nil
}
let uppercaseDesign = getSubteamLead(subteam: "design").uppercased()let uppercaseDesign = getSubteamLead(subteam: "design")?.uppercased()let designLead = getSubteamLead(subteam: "design") ?? "Invalid"/// A structure that represents a Google Analytics event.
struct Event {
/// The name of the event.
let name: String
/// The parameters to pass in to this event.
let parameters: [String: Any]?
}/// An enumeration representing an Uplift event.
enum UpliftEvent: String {
/// Taps on a gym cell on the home page.
case tapGymCell = "tap_gym_cell"
/// An enumeration representing the type of event.
enum EventType {
case facility
case gym
}
/**
Retrieve an `Event` object for this custom event.
Do not pass in any parameters to this function if no parameters are tracked for this event.
- Parameters:
- type: The type of the event to track used as the parameter key.
- value: The value for the parameter key.
- Returns: An `Event` to be tracked in Google Analytics.
*/
func toEvent(type: EventType? = nil, value: String? = nil) -> Event {
// No Parameters
guard let type,
let value else { return Event(name: rawValue, parameters: nil) }
// With Parameters
var parameters: [String: Any]
switch type {
case .facility:
parameters = ["facilityName": value]
case .gym:
parameters = ["gymName": value]
}
return Event(name: rawValue, parameters: parameters)
}
}/// Manage Google Analytics.
class AnalyticsManager {
/// Shared singleton instance.
static let shared = AnalyticsManager()
private init() {}
/// Log an event to Google Analytics.
func log(_ event: Event) {
#if !DEBUG
Analytics.logEvent(event.name, parameters: event.parameters)
#else
Logger.statistics.info("[DEBUG] Logged event: \(event.name), params: \(event.parameters?.description ?? "nil")")
#endif
}
}// Assume `gym` is a `Gym` object with a `name` property...
AnalyticsManager.shared.log(
UpliftEvent.tapGymCell.toEvent(type: .gym, value: gym.name)
)TODO 7: containsNum
_ / 4
TODO 8: uppercaseLead
_ / 6
TODO 9: filterImposter
_ / 6
Feedback Survey
_ / 1
SUBTOTAL
_ / 10
Deduction: Crash Tax
-1 point
GRAND TOTAL
_ / 10












Services: service helpers such as a Networking API service, CoreData, SwiftData, UserDefaults, etc.
Networking: our app’s Networking API service. No need to create this folder if there is only one file.
Utils: other helper files such as constants, extensions, custom errors, etc.
ViewModels: our app’s view models which implement properties and commands to which the view can data bind to and notify the view of any state changes. If using MVC, replace this with Controllers.
Inside of this folder, we can create a folder for each “main” section of our app.
Views: the appearance and UI of the app.
Inside of this folder, we can create a folder for each “main” section of our app.
We can also create a Supporting folder which contains reusable views that are used throughout the app.
Analytics + Crashlytics
DEV_URL = https:/$()/mybackendserver-dev.com
PROD_URL = https:/$()/mybackendserver-prod.com/// An enumeration representing Uplift's environment variables.
enum UpliftEnvironment {
/// Keys from Info.plist.
enum Keys {
#if DEBUG
static let baseURL: String = "DEV_URL"
#else
static let baseURL: String = "PROD_URL"
#endif
}
/// A dictionary storing key-value pairs from Info.plist.
private static let infoDict: [String: Any] = {
guard let dict = Bundle.main.infoDictionary else {
fatalError("Info.plist not found")
}
return dict
}()
/**
The base URL of Uplift's backend server.
* If the scheme is set to DEBUG, the development server URL is used.
* If the scheme is set to RELEASE, the production server URL is used.
*/
static let baseURL: URL = {
guard let baseURLString = UpliftEnvironment.infoDict[Keys.baseURL] as? String,
let baseURL = URL(string: baseURLString) else {
fatalError("Base URL not found in Info.plist")
}
return baseURL
}()
}if let path = Bundle.main.path(forResource: "<NAME_OF_FILE>", ofType: "plist"),
let dict = NSDictionary(contentsOfFile: path) as? [String: AnyObject] {
// Use dictonary here...
}extension Color {
/// Singleton instance
static let volume = Volume()
/// A structure containing colors used in Volume's design system.
struct Volume {
let backgroundGray = Color(white: 245 / 255)
let buttonGray = Color(white: 238 / 255)
let buttonDisabledGray = Color(red: 244/255, green: 239/255, blue: 239/255)
let errorRed = Color(red: 203/255, green: 46/255, blue: 46/255)
}
}/// A structure containing constants used in Volume's design system.
struct Constants {
/// Colors used in Volume's design system.
enum Colors {
// Primary
static let backgroundGray = Color(white: 245 / 255)
static let buttonGray = Color(white: 238 / 255)
static let buttonDisabledGray = Color(red: 244/255, green: 239/255, blue: 239/255)
static let errorRed = Color(red: 203/255, green: 46/255, blue: 46/255)
}
enum Fonts {
// H Headers
static let h0 = Font.custom("Montserrat-Bold", size: 36)
static let h1 = Font.custom("Montserrat-Bold", size: 24)
static let h2 = Font.custom("Montserrat-Bold", size: 16)
static let h3 = Font.custom("Montserrat-Bold", size: 14)
}
}Our custom class needs to be a subclass of UITableViewCell
class CustomTableViewCell: UITableViewCell { }
Create the following initializer:
Determine what views to create and write a helper function to initialize its properties.
For example, if we need to display some text, we would create a UILabel and create a helper function to initialize its font, font color, etc. Note that we do not know anything about the data yet, so the property .text of the UILabel will not be initialized yet.
Inside of the helper function, add the views we created as a subview to contentView and constrain the view with respect to contentView. Then call the helper function inside of the initializer.
This is one of the main differences from what we have been doing before. Instead of referencing view, we will be using contentView. Note that we do not need to use safeAreaLayoutGuide here.
Create a configure function (do not make private) that will take in some data as a parameter, and configure our views.
For example, we could write a function that takes in a String and sets UILabel.text property equal to the value passed in.
Create a reuse identifier for this cell: static let reuse = "<reuse_identifier>"
See “Dequeuing Cells” below for more information.
In Step 6 above, notice that we have a reuse identifier. First, let’s imagine we have a table view that contains a list of all students at Cornell. How many cells would we have? Thousands! Remember, each cell is a separate view and if we have thousands of views, that’s a lot of memory being used! The workaround for this would be to create only the views needed on the screen at one time. If a cell were to go off of the screen, Swift will dequeue this cell for another cell to be created. This is the reason why the cells in a UITableView look very similar! It makes it very efficient to dequeue a cell and reuse it.
How does Swift know which cell to “pick up” and reuse? A reuse identifier is used to associate a cell that is being dequeued with another cell that is about to be rendered.
A UITableView is just like any other UIView that we've worked with thus far. We've initialized the view by doing the following steps:
Create the view
Configure the view by changing its properties
Adding the view as a subview to some parent view
Enable auto layout and set up constraints
With a UITableView, we do the exact same thing but with 3 additional steps:
Register a UITableViewCell
For example, if we had a custom class called CustomTableViewCell with a static reuse constant called reuse, we would use the following code:
Set the UITableView delegate (create an extension just like any other protocol)
See UITableViewDelegate section below
Set the UITableView dataSource (create an extension just like any other protocol)
See section below
The purpose of a UITableViewDelegate is to add functionality to the table view. A class conforming to the protocol UITableViewDelegate does not have any required functions to implement; however, the two most common functions to implement are: heightForRowAt and didSelectRowAt.
In contrast to UITableViewDelegate, there are two required functions to implement: cellForRowAt and numberOfRowsInSection.
For numberOfRowsInSection, we want to provide the number of rows (cells) for a section. Usually, this is the size of our data model array. For example, if our table view listed out all students at Cornell, we would have a data model representing an array of Student objects. The number of rows would be the size of the array (use .count to get the size).
The purpose of cellForRowAt is to determine the cell class to use (in addition to registering the cell) as well as configuring the cell (by calling the configure function). The following code is for a custom cell class called CustomTableViewCell:
Let’s go over this function line by line:
First, we dequeue the cell with the given reuse identifier. This gives us a UITableViewCell. Now, we need to cast it to our custom type by using the as? keyword. This returns an optional type of our custom cell. We unwrap it by using a guard let (or we could use an if let). If the casting failed and the optional holds nil, we return a basic UITableViewCell.
Then, we need to identify which data this cell will hold. Most of the time, we will have some data model array (such as an array of students). To determine the position a cell is located inside of a table view, we use indexPath.row which returns an Int. We could then use this value to access an element inside of our data model array.
Next, we would need to configure our cell with the data model that we retrieve in Step 2. We can pass this information into our custom cell class’s configure function that we implemented earlier to configure the cell’s views such as changing a UILabel’s text.
Finally, we return the configured custom cell.
Fall 2023 | Vin Bui
Because SwiftUI is declarative, the way we write the logic in our apps is completely different from how we would do it in UIKit. Let’s take a look at the following code that increments the value inside of a Text by 1 every time we click on a button:
The above code will give us an error for the line containing count += 1 indicating that ‘self’ is immutable. Why? That’s because we are using a struct. When we change the value of a property inside of a struct, the entire struct changes. If that’s the case, then how do we keep the struct object alive while being able to change the value of its properties? We use property wrappers.
To make the above code work, we can add the @State property wrapper to the count property. Because this object creates and owns this property, we should mark it as private for good practice:
The powerful thing about using these property wrappers is that SwiftUI automatically updates the views for us. When we incremented the value of count, the Text view using this property gets updated automatically.
Now, let’s change things up and use a String with a TextField:
If we try to run the above code, it will not work. That’s because the name variable that we pass into the TextField is a String and NOT Binding<String>. What does this “binding” mean? Let’s think about the difference between a Text and a TextField. A Text only reads the value that we pass in whereas a TextField reads AND writes a value. In this case, we must add a $ to perform a two-way binding (in other words, a read and a write):
You may have noticed that we only used value types in the examples above. That’s because @State only works for value types such as structs, strings, ints, etc. However, there are times when we want to pass one reference around to multiple views. In this case, we want to use a reference type.
Consider the following code:
If we change the values inside of the text fields, the Text view will not be updated properly. One way to fix this is to change the User class to a struct. However, we want to keep it as a reference type.
When dealing with reference types, we must use a different property wrapper: @StateObject. To properly use this property wrapper, we must do the following (iOS 16 and below):
Conform our class to the ObservableObject protocol.
Mark the properties we want to observe with @Published.
Use @StateObject on our reference type property.
You may be wondering, “Why does @State work for structs but not classes?” When we use @State SwiftUI observes the entire value. When we change the property of a struct, the entire value changes since structs are value types. On the other hand, when we change the property of a class, the entire object does not change and a new copy is not created like it does with structs.
The above property wrappers that we used only works if the view creates and owns the property. However, properties are often shared among views. If one child view changes a property, we also want to update it in the parent view. There are also different ways of sharing properties: (1) either directly (1-on-1) from a parent to child or (2) shared among every view through the environment.
Below is a list of some of the most common property wrappers and when we would use them.
The view itself creates and owns the instance we want to wrap.
Property being wrapped is a value type (such as struct or enum).
Mark as private for best practice. No external source should modify @State properties.
Similar to @State but used on an ObservableObject (reference types).
Changes to @Published properties are notified.
The view itself creates and owns the instance.
@State Creates → @Binding Receives.
Read and write to a property that's owned by a parent view.
Property being wrapped is a value type (such as struct or enum).
@StateObject Creates → @ObservedObject Receives.
Similar to @Binding but used on an ObservableObject (reference types).
Changes to @Published
Similar to @ObservedObject but shared to MULTIPLE views.
The view itself DOES NOT create or own the instance.
Where does the data come from?
Owned
Immutable Value? Regular Property
Mutable Value? @State
Starting with iOS 17, we longer need to mark our class properties with @Published and conform our class to ObservableObject. All we have to do is add the @Observable property wrapper to our class. Additionally, we can just simply use @State on the class object.
Below is an iOS 17 version of the code we used earlier:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
// `required init` here// 1. Subclass of `UITableViewCell`
class CustomTableViewCell: UITableViewCell {
// 3. Create view properties
private let label = UILabel()
// 6. Create a reuse identifier
static let reuse = "CustomTableViewCellReuse"
// 2. Create the following init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// 4. Call helper functions
setupLabel()
}
// 2. `required init` here
// 5. `configure` function (do not make private)
func configure(newText: String) {
label.text = newText
// Configure additional views here
}
// 3. Set Up View Helpers
private func setupLabel() {
// 3. Initialize the label's properties
// 4. Add as subview to `contentView`
// 4. Constrain with respect to `contentView`
}
}tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.reuse)// If our cell has a fixed height, then implement this function.
// If our cell has a dynamic height, do not implement this function and rely on AutoLayout.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return <height> // the height of the cell
}func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Perform some operation when cell is tapped
}func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataModelArray.count
}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.reuse, for: indexPath) as? CustomTableViewCell else { return UITableViewCell() }
let dataModel = dataModelArray[indexPath.row]
cell.configure(...) // pass in our dataModel to the configure function in our custom cell class
return cell
}class ViewController: UIViewController {
// 1. Create the view
private let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// Additional setup here
setupTableView() // 2. Configure the view
}
// 2. Configure the view
private func setupTableView() {
// 5. Register Custom Cell
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.reuse)
tableView.delegate = self // 6
tableView.dataSource = self // 7
view.addSubview(tableView) // 3
tableView.translatesAutoresizingMaskIntoConstraints = false // 4
// 4. Set constraints
}
}
// 6. Conform to `UITableViewDelegate`
extension ViewController: UITableViewDelegate {
// `heightForRowAt` (optional)
// `didSelectRowAt` (optional)
// Additional functions here
}
// 7. Conform to `UITableViewDataSource`
extension ViewController: UITableViewDataSource {
// `cellForRowAt`
// `numberOfRowsInSection`
// Additional functions here
}struct ContentView: View {
var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Add")
}
}
}
}Use $ for a two-way binding to the property (read and write).
Use $ for a two-way binding to the property (read and write).
The view itself DOES NOT create or own the instance.
Parent
Immutable Value? Regular Property
Mutable Value? @Binding
ObservableObject? @ObservedObject
Environment (@Environment is to @EnvironmentObject → @State is to @StateObject)
EnvironmentalValues, keyPath? @Environment
ObservableObject? @EnvironmentObject
Saved on Disk
UserDefaults?
Whole app? @AppStorage
Single scene (for MacOS)? @SceneStorage
CoreData? @FetchRequest
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Add")
}
}
}
}struct ContentView: View {
@State private var name: String = "Vin"
var body: some View {
VStack {
Text("Hello \(name)")
TextField("Change Me", text: name)
}
}
}TextField("Change Me", text: name) // Incorrect
TextField("Change Me", text: $name) // Correctclass User {
var firstName: String = "Vin"
var lastName: String = "Bui"
}
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("Hello \(user.firstName) \(user.lastName)")
TextField("First Name", text: $user.firstName)
TextField("Last Name", text: $user.lastName)
}
}
}class User: ObservableObject {
@Published var firstName: String = "Vin"
@Published var lastName: String = "Bui"
}
struct ContentView: View {
@StateObject private var user = User()
var body: some View {
VStack {
Text("Hello \(user.firstName) \(user.lastName)")
TextField("First Name", text: $user.firstName)
TextField("Last Name", text: $user.lastName)
}
}
}@Observable
class User {
var firstName: String = "Vin"
var lastName: String = "Bui"
}
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("Hello \(user.firstName) \(user.lastName)")
TextField("First Name", text: $user.firstName)
TextField("Last Name", text: $user.lastName)
}
}
}Only Steps 1 and 2 are different when creating a UICollectionViewCell compared to a UITableViewCell
Our custom class needs to be a subclass of UICollectionViewCell
class CustomCollectionViewCell: UICollectionViewCell { }
Create the following initializer:
Determine what views to create and write a helper function to initialize its properties.
For example, if we need to display some text, we would create a UILabel and create a helper function to initialize its font, font color, etc. Note that we do not know anything about the data yet, so the property .text of the UILabel will not be initialized yet.
Inside of the helper function, add the views we created as a subview to contentView and constrain the view with respect to contentView. Then call the helper function inside of the initializer.
This is one of the main differences from what we have been doing before. Instead of referencing view, we will be using contentView. Note: we do not need to use safeAreaLayoutGuide here.
Create a configure function (do not make private) that will take in some data as a parameter, and configure our views.
For example, we could write a function that takes in a String and sets UILabel.text property equal to the value passed in.
Create a reuse identifier for this cell: static let reuse = "<reuse_identifier>"
See “Dequeuing Cells” below for more information.
The idea behind this concept is the exact same for that of a UITableViewCell. Read it here:
A UICollectionView is just like any other UIView that we've worked with thus far. We've initialized the view by doing the following steps:
Create the view
Configure the view by changing its properties
Adding the view as a subview to some parent view
Enable auto layout and set up constraint
With a UICollectionView, we do the exact same thing but with additional steps:
Register a UICollectionViewCell
For example, if we had a custom class called CustomCollectionViewCell with a static reuse constant called reuse, we would use the following code:
Set the UICollectionView delegate (create an extension just like any other protocol)
See UICollectionViewDelegate section below
Set the UICollectionView dataSource (create an extension just like any other protocol)
See section below
Every step that we mentioned above is very similar to that of a UITableViewCell; however, there is 1 more additional step.
Initialize the collection view with a UICollectionViewFlowLayout and conform to UICollectionViewDelegateFlowLayout
See UICollectionViewFlowLayout section below
The purpose of a UICollectionViewDelegate is to add functionality to the collection view. A class conforming to the protocol UICollectionViewDelegate does not have any required functions to implement; however, the most common function to implement is: didSelectItemAt.
In contrast to UICollectionViewDelegate, there are two required functions to implement: cellForItemAt and numberOfItemsInSection. The idea is exactly the same as that of a table view, but with minor syntax changes (”item” instead of “row”).
Read the details here:
Everything we’ve mentioned earlier is very similar to a table view but with minor syntax changes. However, there is one more additional step that is required by a collection view that gives it customization benefits.
Inside of the helper function that sets up the collection view, add the following lines of code:
Let’s go over this line by line:
Create a UICollectionViewFlowLayout instance
(REQUIRED) Set the collection view’s scroll direction: .vertical or .horizontal
(OPTIONAL) Set the spacing between each line (top and bottom)
(OPTIONAL) Set the spacing between each item (left and right)
Initialize CollectionView with the layout we just created
In the previous step, we configured the collection view’s layout. Remember how the UICollectionViewDelegate did not have a heightForRowAt function like a table view does? Well that’s because each item (cell) has a customizable height and width whereas in a table view, we could only customize the row’s height. To do this, just create an extension and conform to UICollectionViewDelegateFlowLayout and add this function:
A lot of the material mentioned above is very repetitive and already seen in a UITableView. For comparison purposes, here are the main differences between the two when setting them up:
Create a subclass of UICollectionViewCell instead
The init function is different
A flow layout is required when initializing the collection view
You may have noticed that instead of using private let collectionView = UICollectionView() we used private var collectionView: UICollectionView!. The reason for this is because we have to pass in a layout when initializing the collection view. By replacing it with this line, we are making a promise that we will initialize the collection view later. (There is a cleaner way to do this but it is a bit advanced for now).
You must conform to UICollectionViewDelegateFlowLayout
Implement the sizeForItemAt function
Syntax: use “items” instead of “rows”
Ex: cellForItemAt instead of cellForRowAt. However, the implementation is the exact same.
Fall 2023 | Reade Plunkett
WidgetKit is the framework developed by Apple that allows us to build widgets, watch complications, and Live Activities. It allows contents of our app to be available in contexts outside the app and extend its reach by building an ecosystem of glanceable, up-to-date experiences.
While Widgets use SwiftUI to display their content, WidgetKit is used to render their view. Even if your widget is on screen, it is not necessarily active at all times. Since reloading a widget consumes system resources and can lead to battery drain, WidgetKit limits the frequency and number of updates you request to what’s necessary.
Each widget is assigned a budget which is dynamically allocated and considers the following factors:
The frequency and times the widget is visible to the user
The widget’s last reload time
Whether the widget’s containing app is active
Typically a widget can be allocated 40 - 70 refreshes as a daily budget, corresponding to the widget reloading about every 15 - 60 minutes.
We can define a timeline for when our widget should update if the widget has predictable points in time where it makes sense to update its content. For example, a widget that displays weather information might update the temperature hourly throughout the day. A stock market widget could update its content frequently during open market hours, but not at all over the weekend. By planning these times in advance, WidgetKit automatically refreshes your widget when the appropriate time arrives.
When you define your widget, you implement a custom . WidgetKit gets a timeline from your provider, and uses it to track when to update your widget. A timeline is an array of objects. Each entry in the timeline has a date and time, and additional information the widget needs to display its view. In addition to the timeline entries, the timeline specifies a refresh policy that tells WidgetKit when to request a new timeline.
Before we start creating our widget, we will create a Weather Model that will contain the data for our weather. The model contains the following information:
conditions: A string describing the weather conditions.
symbol: A string referencing an that depicts the conditions.
color: The background color name for these weather conditions.
We also define six constant weather conditions within our model that we will reference through this project corresponding to sunny, cloudy, overcast, rainy, lightning, and snowy.
Next, we will define TimelineEntry for our weather widget. This type specifies the date to display a widget, and, optionally, indicates the current relevance of the widget’s content.
Here, we define an entry that contains the date as well as a weather model that corresponds to this entry. We will use the data in the weather model to populate our Widget’s content view.
Periodically, our widgets will need to update their display. For example, a sports widget could update its display every time a team scores, a weather widget could update when the conditions change, or a music widget could update when the song playing finishes. The TimelineProvider is a type that advises WidgetKit when to update a widget’s display.
WidgetKit requests a timeline from the provider. This timeline is an array of objects that conform to TimelineEntry. Each entry contains a date, as well as additional properties we can define for displaying the widget.
Within the Provider, we have three functions to handle each of the three ways WidgetKit can request timeline entries: placeholder, getSnapshot , andgetTimeline
placeholder: returns an entry representing a placeholder version of the widget.
getSnapshot: returns single immediate snapshot entry that represents the widget’s current state.
getTimeline: returns a timeline of entries, including the current moment and any future dates when the widget’s state will change.
When WidgetKit displays our widget for the first time, it renders the widget’s view as a placeholder. This placeholder provides a generic representation of your widget, giving the user a general idea of what the widget shows.
WidgetKit calls getSnapshot when the widget appears in transient situations, such as when the user is adding a widget. The context parameter provides additional details how the entry should be use, for example, whether it is a preview inside the Widget Gallery, the family, or the size of the widget to display.
In this snapshot, we create and return an entry that contains the current date and time, as well as a random Weather object. It is important to note, if the data for the snapshot requires a significant amount of time to load (for example, making a network call), it is best practice to use sample data.
After a user adds our widget from the widget gallery, WidgetKit makes the timeline request. Our widget extension will not always be running, so WidgetKit needs to know when to activate to update the widget. The timeline tells WidgetKit when you would like to update the widget.
Here, we create and return a timeline of five entries each an hour apart. Each entry contains a date as well as a random weather. We use a refresh policy .atEnd to tell WidgetKit to request a new timeline after the last date specified by the entries in the timeline.
Let’s first start by building the Widget itself by conforming to the Widget protocol. This protocol contains two components:
kind: This is a string that identifies the widget.
body: This is body of the widget that defines its contents and configuration.
A Widget can have two configurations: StaticConfiguration or AppIntentConfiguration.
StaticConfiguration: Describes the content of a widget that has no user-configurable options.
AppIntentConfiguration: Describes the content of a widget that uses custom intent to provide user-configurable options.
We pass into the configuration the widget’s kind as well as its TimelineProvider. WidgetKit invokes the content closure, passing a timeline entry created by the widget’s provider. This entry can either be a snapshot entry or one from the timeline. Once we have this entry, we can pass it into the Widget’s view to display its contents.
For a widget, we have a view notable modifiers:
configurationDisplayName: Sets the name shown for a widget when a user adds or edits it.
description: Sets the description shown for a widget when a user adds or edits it.
supportedFamilies: Sets the sizes that a widget supports. There are two families our widget can belong in: system or accessory.
Next, we create the View for our widget using SwiftUI. The Widget’s entry gets fetched from the TimelineProvider and passed into the View by the Widget’s StaticConfiguration.
We can access the Weather object within our Entry to display the temperature, conditions, symbol, and background color.
Additionally, we can make use of the canvas and live view to easily and quickly view our changes. We can even specify which size Widget (or WidgetFamily) we want to see displayed in the preview:
Here, we create an entry in the timeline for each one of our weather conditions. We can view and cycle through this timeline in the preview.
In the previous section, we discussed creating a new widget extension. We can define multiple widgets within a single widget extension. A widget bundle allows us to expose these multiple widgets. In this case however, we only expose a single widget.
// Compare this with a `UITableViewCell`
override init(frame: CGRect) {
super.init(frame: frame)
}
// `required init` here// 1. Subclass of `UICollectionViewCell`
class CustomCollectionViewCell: UICollectionViewCell {
// 3. Create view properties
private let label = UILabel()
// 6. Create a reuse identifier
static let reuse = "CustomCollectionViewCellReuse"
// 2. Create the following init
override init(frame: CGRect) {
super.init(frame: frame)
// 4. Call helper functions
setupLabel()
}
// 2. `required init` here
// 5. `configure` function (do not make private)
func configure(newText: String) {
label.text = newText
// Configure additional views here
}
// 3. Set Up View Helpers
private func setupLabel() {
// 3. Initialize the label's properties
// 4. Add as subview to `contentView`
// 4. Constrain with respect to `contentView`
}
}collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.reuse)func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Perform some operation when cell is tapped
}func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataModelArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withIdentifier: CustomCollectionViewCell.reuse, for: indexPath) as? CustomCollectionViewCell else { return UICollectionViewCell() }
let dataModel = dataModelArray[indexPath.row]
cell.configure(...) // pass in our dataModel to the configure function in our custom cell class
return cell
}let layout = UICollectionViewFlowLayout()
layout.scrollDirection = // .vertical or .horizontal
layout.minimumLineSpacing = // (optional) spacing amount
layout.minimumInteritemSpacing = // (optional) spacing amount
// Initialize CollectionView with the layout
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: <width>, height: <height>)
}class ViewController: UIViewController {
// 1. Create the property BUT DONT INITIALIZE IT YET
// Note that this is different from a table view
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// Additional setup here
setupCollectionView() // 2. Configure the view
}
// 2. Configure the view
private func setupCollectionView() {
// 8. Create a FlowLayout
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = // .vertical or .horizontal
layout.minimumLineSpacing = // (optional) spacing amount
layout.minimumInteritemSpacing = // (optional) spacing amount
// Initialize CollectionView with the layout
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(CustomTableViewCell.self, forCellWithReuseIdentifier: CustomTableViewCell.reuse) // 5
collectionView.delegate = self // 6
collectionView.dataSource = self // 7
view.addSubview(collectionView) // 3
collectionView.translatesAutoresizingMaskIntoConstraints = false // 4
// 4. Set constraints
}
}
// 6. Conform to `UICollectionViewDelegate`
extension ViewController: UICollectionViewDelegate {
// `didSelectItemAt` (optional)
// Additional functions here
}
// 7. Conform to `UICollectionViewDataSource`
extension ViewController: UICollectionViewDataSource {
// `cellForItemAt`
// `numberOfItemInSection`
// Additional functions here
}
// 8. Conform to `UICollectionViewDelegateFlowLayout`
extension ViewController: UICollectionViewDelegateFlowLayout {
// `sizeForItemAt`
// Additional functions here
}temp: The current temperature
System Family
systemSmall: A small system widget that can appear on the Home Screen, Today View, or in StandBy, or the Mac Desktop.
systemMedium: A medium system widget that can appear on the Home Screen, Today View, or in StandBy, or the Mac Desktop.
systemLarge: A large system widget that can appear on the Home Screen, Today View, or in StandBy, or the Mac Desktop.
systemExtraLarge: An extra large system widget that can appear on the Home Screen, Today View, StandBy, or the Mac Desktop.
Accessory Family
accessoryCircular: A circular widget that can appear as a complication in watchOS, or on the Lock Screen.
accessoryCorner: A widget-based complication in the corner of a watch face in watchOS.
import Foundation
struct Weather {
let conditions: String
let symbol: String
let color: String
let temp: Int
static let sunny = Weather(conditions: "Sunny", symbol: "sun.max.fill", color: "sunny-color", temp: 78)
static let cloudy = Weather(conditions: "Cloudy", symbol: "cloud.sun.fill", color: "cloudy-color", temp: 64)
static let overcast = Weather(conditions: "Overcast", symbol: "cloud.fill", color: "overcast-color", temp: 45)
static let rainy = Weather(conditions: "Rainy", symbol: "cloud.rain.fill", color: "rainy-color", temp: 62)
static let lightning = Weather(conditions: "Lightning", symbol: "cloud.bolt.fill", color: "lightning-color", temp: 57)
static let snowy = Weather(conditions: "Snowy", symbol: "snowflake", color: "snowy-color", temp: 18)
}import WidgetKit
struct WeatherEntry: TimelineEntry {
let date: Date
let weather: Weather
}import WidgetKit
struct Provider: TimelineProvider {
}func placeholder(in context: Context) -> WeatherEntry {
return WeatherEntry(date: Date(), weather: .sunny)
}func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) {
let entry = WeatherEntry(date: Date(), weather: .sunny)
completion(entry)
}func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
var entries: [WeatherEntry] = []
let possibleWeathers: [Weather] = [.sunny, .cloudy, .overcast, .rainy, .lightning, .snowy]
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = WeatherEntry(date: entryDate, weather: possibleWeathers.randomElement()!) // select a random weather
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}import WidgetKit
import SwiftUI
struct WeatherWidget: Widget {
let kind: String = "WeatherWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WeatherWidgetView(entry: entry)
}
.configurationDisplayName("Forecast")
.description("View the weather forecast for your location.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular])
}
}import SwiftUI
import WidgetKit
struct WeatherWidgetView : View {
var entry: Provider.Entry
var body: some View {
VStack {
HStack {
Text("\\(entry.weather.temp)°")
.font(.system(size: 32))
.minimumScaleFactor(0.5)
Spacer()
Image(systemName: entry.weather.symbol)
.resizable()
.symbolRenderingMode(.multicolor)
.aspectRatio(contentMode: .fit)
.frame(minWidth: 20, maxWidth: 40, minHeight: 20, maxHeight: 40)
}
Spacer()
HStack {
Text(entry.weather.conditions)
.font(.system(size: 16, weight: .bold))
.minimumScaleFactor(0.5)
Spacer()
}
}
.foregroundStyle(.white)
.containerBackground(Color(entry.weather.color).gradient, for: .widget)
}
}#Preview(as: .systemSmall) {
WeatherWidget()
} timeline: {
WeatherEntry(date: .now, weather: .sunny)
WeatherEntry(date: .now, weather: .cloudy)
WeatherEntry(date: .now, weather: .overcast)
WeatherEntry(date: .now, weather: .rainy)
WeatherEntry(date: .now, weather: .lightning)
WeatherEntry(date: .now, weather: .snowy)
}import WidgetKit
import SwiftUI
@main
struct WeatherWidgetBundle: WidgetBundle {
var body: some Widget {
WeatherWidget()
// A second widget...
// A third widget...
}
}accessoryRectangular: A rectangular widget that can appear as a complication in watchOS, or on the Lock Screen.
accessoryInline: An inline widget that can appear as a complication in watchOS, or on the Lock Screen.
Original Author: Vin Bui
Assignment Due: Thursday Oct 30, 2025 11:59pm
In this assignment, you will be creating your first ever iOS application using UIKit programmatically. You will be creating a Profile and Edit Profile page, commonly seen in many apps today.
Developer Skills
How to format and structure your code to follow MVC design pattern
How to follow common styling conventions used in industry
How to implement designs created on Figma
How to work with Git and GitHub for version control
Course Material
How to create classes such as a UIViewController
How to create and customize a UIView and position them with NSLayout
As with any other course at Cornell, the Code of Academic Integrity will be enforced in this class. All University-standard Academic Integrity guidelines should be followed. This includes proper attribution of any resources found online, including anything that may be open-sourced by AppDev. The University guidelines for Academic Integrity can be found .
This assignment can be done with ONE partner. You are also free to come to the instructors or any course staff for help. Programming forums like Stack Overflow or Hacking with Swift are allowed as long as you understand the code and are not copying it exactly. The majority of code (excluding external libraries) must be written by you or your partner. Code written through AI means such as ChatGPT is NOT ALLOWED. However, you may use these resources for assistance, although we highly encourage consulting Ed Discussion or office hours instead.
If you are stuck or need a bit of guidance, please make a post on Ed Discussion or visit . Please do not publicly post your code on Ed Discussion. If you are using an external resource such as Stack Overflow, keep in mind that we are using UIKit with Swift 5. If you see anything with @IBOutlet or any weird syntax, then you are most likely looking at a different version.
The feedback form link is located in the section of this handout.
UI: implements the user interface
F: implements the functionality
EC: extra credit
You can find the link to the Figma . If you do not have an account, you can create one under your Cornell email. I will provide details on how to navigate through Figma later.
If you are having trouble understanding how we will be using Git in this course, please read the , or visit office hours so we can assist you. As a reminder:
Stage: git add .
Commit: git commit -m "YOUR MESSAGE HERE"
Push: git push
Navigate to a folder on your device where you will keep all of your assignments. You can navigate to the folder using cd in Terminal.
Clone the repository on GitHub:
Replace NETID with your NetID
Replace SEM with the semester (in this case sp25)
If you have a partner, replace NETID1 and NETID2. Try changing the order if the former does not work.
If you are lost or getting any kind of error, create a post on Ed Discussion or come to office hours.
Navigate to the repository located on your local computer drive. Inside of the folder NETID-a2 should contain an Xcode project called A2.xcodeproj. Open up the project.
Once you have the project opened, on the left side of the screen you should see the Navigator which contains all of the folders and files in the directory. If not, press CMD + 0 (that’s a zero) on your keyboard.
If you expand everything underneath A2 you should see the following:
You will be working on ProfileVC.swift, EditProfileVC.swift, and Assets.xcassets.
ProfileVC.swiftYou will be creating the main profile page in this file, primarily in Parts I and II. You are responsible for creating the UI design based on the . This view controller is the root view controller inside of a UINavigationController located in SceneDelegate.swift. You will be asked to push EditProfileVC onto this navigation stack.
EditProfileVC.swiftYou will be creating the edit profile page in this file, primarily in Parts III and IV. You will be implementing the UI design based on the . This view controller will be pushed by ProfileVC onto the navigation stack. You will be asked to implement popping functionality as well as delegation to save changes from the text field.
UIColor+Extension.swiftDO NOT EDIT THIS FILE! This file contains colors that are featured in the Figma design. To use the colors, simply type UIColor.a2.<color_name>. It is good practice to implement the design system before starting any project, making it very easy to use throughout the entire project. Look over this file to understand how it works and keep note of the colors available for you to use.
For the scope of this course, we will be teaching you the skills necessary to read a design implemented on Figma. This is widely used both on AppDev and in industry, so it’s important to have this skill in your toolkit. Please read over the now.
Throughout the provided files, you may have noticed the // MARK comments. These are used to keep the code organized.
Properties (View) are used for UIView objects such as UILabel, UIImageView, etc. You should mark these properties as private and make them constants (use let).
You are not limited to these sections and are free to add more (and you should). Because many of your data properties are marked as private, you may need to create an init function.
Follow these steps when implementing the UI:
Create the view
Initialize the view
Constrain the view
Run, confirm, and repeat
Your viewDidLoad method should contain mostly function calls to helper functions. We will be grading you on this.
Your task is to create the UI for the main profile page in ProfileVC. This profile can be for you, your partner, or if you want you can use me (Vin). Do not worry about any functionality here. We will do that in Part II. Your profile will have the following:
Profile Image: UIImageView
You will need to add the image to Assets.xcassets. Refer to the Figma guide.
To get a perfect circle, set the
Don’t forget to set the title of the view controller to “My Profile” and background color.
Once you are done, stage, commit, and push to GitHub.
You task is to create the “Edit Profile” button as well as pushing EditProfileVC onto the navigation stack.
Edit Profile Button: UIButton
To change the text, use setTitle(<text>, for: .normal)
To change the text color, use setTitleColor(<color>, for: .normal)
To change the background color, use backgroundColor = <color>
Once you are done, stage, commit, and push to GitHub.
Your task is to create the UI for the edit profile page in EditProfileVC. Do not worry about any functionality here. We will do that in Part IV. Consult Part I for hints on how to implement these views. This page will have the following:
Profile Image: UIImageView
Name: UILabel
Bio: UILabel (or
You will need to create a data property to store some information. Mark these properties as private and create an init function. Make sure to include the following line after initializing your properties: super.init(nibName: nil, bundle: nil). The values for these properties will be passed in from ProfileVC.
Don’t forget to set the title of the view controller to “Edit Profile” and background color.
Once you are done, stage, commit, and push to GitHub.
You task is to create the “Save” button as well as popping EditProfileVC from the navigation stack.
Save Button: UIButton
See Part II for implementation hints
Once you are done, stage, commit, and push to GitHub.
You task is to use delegation to update information from ProfileVC based on the text fields in EditProfileVC. Remember these steps:
Create a protocol with a function
Conform ProfileVC to the protocol (delegate)
Implement the function
If you have forgotten how to implement delegation, view the lecture notes or textbook.
To access the text from a UITextField, use the text property of the text field. Note that this gives you an optional.
Double check that your main profile updates when you click save. Then click on “Edit Profile” again and make sure that the text fields in the edit profile page are also updated.
Once you are done, stage, commit, and push to GitHub.
If you reach this point, you are done with the assignment. However, feel free to challenge yourself with the extra credit features.
Extra credit will only be given if the features are fully implemented. These are unordered and you can choose as many as you like.
When using a UINavigationController, there is a default back button. However, it does not look nice with our design so your task is to customize the back button. The Figma contains the design for this feature. As a hint, the icon used is known as an SF Symbol called chevron.left. You do not need to export this icon; it is built-in.
This one is a lot more challenging than the previous feature. Your task here is to allow the user to edit their profile picture. You can access their camera roll, photo library, or both.
Once you are done, stage, commit, and push to GitHub.
Double check that all of your files are properly pushed to GitHub.
Clone your repository into a separate folder on your local computer drive.
Run your project and make sure that your code does not crash and everything works as needed.
If you are satisfied, download this TXT file and fill it out. Make sure to use the
Confirm that your submission.txt is formatted like the following and submit it on .
Fill out this (worth 1 point).
How to read documentation from outside resources
UILabel, UIButton, UIImageView, UIImage, UITextFieldHow to navigate between view controllers using a UINavigationController and popping/pushing
How to use delegation to communicate between view controllers
How to implement design system using UIFont and UIColor
_ / 1
PART III: Create the Edit Profile Page
_ / 3
UI: Profile Image
_ / 1
UI: Name, Bio
_ / 1
UI: Hometown and Major TextFields
_ / 1
PART IV: Pop the Edit Profile Page
_ / 2
UI: Save Button
_ / 1
F: Pops EditProfileVC
_ / 1
PART V: Delegation
_ / 3
F: Clicking on Save updates the main Profile page
_ / 3
OTHER
_ / 2
Feedback Survey
_ / 1
Styling: viewDidLoad calls helper functions
_ / 1
SUBTOTAL
_ / 15
EC: Custom back button
+ 1
EC: Edit profile picture
+ 1
Deduction: Crash Tax
-1 point
GRAND TOTAL
_ / 15 (+2)
Properties (Data) are used for data types such as String, Int, delegates, etc. Again, mark these properties as private but it is up to you to decide if they are constants or variables.
The Set Up Views section should be used for initializing your view properties.
UIImageViewlayer.masksToBoundstrueName: UILabel
You can get the colors from Figma under the “Inspect” section. To use the color, type: UIColor.a2.<color_name>
You can get the font weight and size from Figma under the “Inspect” section. Set the “Code” to iOS. To set the font, type: .systemFont(ofSize: <size>, <weight>). Do not use the code. You should only look at the font name and size.
Make sure you use the weight from the font name instead of the number. For example, even though Figma says a weight of 600, the weight should be .semibold.
If any of these fields are too long, you can set the numberOfLines property to 0 for unlimited lines.
Bio: UILabel (or UITextView)
To make the text italic, use: .italicSystemFont(ofSize: <size>)
Hometown: UIImageView for the icon, UILabel for the text
Major: UIImageView for the icon, UILabel for the text
To change the corner radius, use layer.cornerRadius = <radius>. You can get this under “Inspect > Properties” in Figma on the right hand side.
As a hint, you will need to add the following constraints: leading, trailing, bottom, and height (not width)
To add functionality to this button when tapped, use addTarget(self, #selector(<function_to_call>), for: .touchUpInside)
Hometown: UILabel for the text, UITextField for the text field
To set the border width, use layer.borderWidth = <width>
To set the border color, use layer.borderColor = <color>
The color must be a CGColor. Use the following line: UIColor.a2.silver.cgColor
To set the corner radius, use layer.cornerRadius = <radius>
For the text field, you will need to set following constraints: top, leading, trailing, and height (not width)
Creating the padding before the text inside of the textfield is not as straight forward, so it’s okay to not have it. However, if you are interested, check out.
Major: UILabel for the text, UITextField for the text field
EditProfileVC to reference EditProfileVC (delegator)Make sure it has weak before it. If this property is private, make sure to initialize it in the init function.
Call the function in EditProfileVC
PART I: Creating the Profile Page
_ / 3
UI: Profile Image
_ / 1
UI: Name, Bio
_ / 1
UI: Hometown and Major
_ / 1
PART II: Push the Edit Profile Page
_ / 2
UI: Edit Profile Button
_ / 1


F: Pushes EditProfileVC
Original Author: Vin Bui
Midpoint Due: Monday November 17, 2025 11:59 pm Final Due: Tuesday November 25, 2025 11:59 pm
In this assignment, you will be creating a recipe book app. You will be able to fetch recipes from a server, filter by category, and save them for later!
Developer Skills
How to organize your project directory
How to use Postman to test HTTP requests
How to read code written by other developers
How to read data received from the backend to structure frontend code
Course Material
How to set up multiple collection views and communicate between them
How to filter data using higher order functions
How to save data locally using UserDefaults
As with any other course at Cornell, the Code of Academic Integrity will be enforced in this class. All University-standard Academic Integrity guidelines should be followed. This includes proper attribution of any resources found online, including anything that may be open-sourced by AppDev. The University guidelines for Academic Integrity can be found .
This assignment can be done with ONE partner. You are also free to come to the instructors or any course staff for help. Programming forums like Stack Overflow or Hacking with Swift are allowed as long as you understand the code and are not copying it exactly. The majority of code (excluding external libraries) must be written by you or your partner. Code written through AI means such as ChatGPT is NOT ALLOWED. However, you may use these resources for assistance, although we highly encourage consulting Ed Discussion or office hours instead.
If you are stuck or need a bit of guidance, please make a post on Ed Discussion or visit . Please do not publicly post your code on Ed Discussion. If you are using an external resource such as Stack Overflow, keep in mind that we are using UIKit with Swift 5. If you see anything with @IBOutlet or any weird syntax, then you are most likely looking at a different version.
The feedback form link is located in the section of this handout.
UI: implements the user interface
F: implements the functionality
EC: extra credit
You are encouraged to use to test out HTTP requests. Please take a look at the .
Similar to A2 and A3, we will be using Figma for the design sketches. You can find the link to the Figma . If you do not have an account, you can create one under your Cornell email. If you need a refresher, check out the .
If you are having trouble understanding how we will be using Git in this course, please read the A1 handout under section, or visit office hours so we can assist you. As a reminder:
Stage: git add .
Commit: git commit -m "YOUR MESSAGE HERE"
Push: git push
Navigate to a folder on your device where you will keep all of your assignments. You can navigate to the folder using cd in Terminal.
Clone the repository on GitHub:
Replace NETID with your NetID
Replace SEM with the semester (such as fa23 or sp24)
If you have a partner, replace NETID1 and NETID2. Try changing the order if the former does not work.
If you are lost or getting any kind of error, create a post on Ed Discussion or come to office hours.
Navigate to the repository located on your local computer drive. Inside of the folder NETID-a4 should contain an Xcode project called A4.xcodeproj. Open up the project.
Once you have the project opened, on the left side of the screen you should see the Navigator which contains all of the folders and files in the directory. If not, press CMD + 0 (that’s a zero) on your keyboard. You may notice that there is less starter code than in A2 and A3. As developers, directory organization is very important! Look at A2/A3 to see how we organized our directory. Remember to use those // MARK comments!
As mentioned earlier, there is less starter code. You will be required to create your own files and organize them properly. However, there are only two files provided for you.
UIColor+Extension.swiftIn contrast to A3, you are free to edit this file if you want to change the colors. This file contains colors that are featured in the design. To use the colors, simply type UIColor.a4.<color_name>. It is good practice to implement the design system before starting any project, making it very easy to use throughout the entire project. Look over this file to understand how it works and keep note of the colors available for you to use.
UIFont+Extension.swiftThis extension allows you to use the SF Pro Rounded font which is used in the Figma design. To use this font, simply add .rounded to the end of the UIFont. For example, you can do .systemFont(ofSize: 12, weight: .semibold).rounded.
The starter code should have Alamofire, SnapKit, and SDWebImage installed. To use these libraries, use the import statement at the top of the file. You are not required to use SnapKit, but it would save you a lot of time learning how to use it over NSLayout.
Your task is to create a UICollectionView to display the recipes. We will not guide you as much as we did with the other assignments, but keep the following in mind:
You are not required to implement the bookmark icon until Part V, but you are free to do so now.
You will not be implementing the filters until Part III.
You will need to create dummy data. For the sake of time and convenience, I have them written out for you in this . Make sure your model aligns with the given dummy data since the JSON you will be fetching from follows this format.
While creating this assignment, I ran into a bug with the collection view. Make sure to set the collection view’s alwaysBounceVertical property to true.
There are many ways to download images in Swift, but the easiest way in my opinion is using SDWebImage. Using this library is very simple.
Import the library using import SDWebImage at the top of the file.
Given a UIImageView, simply use the .sd_setImage(with: <URL>) function. Here is an example:
Once you are done, stage, commit, and push to GitHub.
Your task is to create a view controller representing a detailed recipe view. You will push this view controller when tapping on the collection view cell. This detailed view will be unique to the recipe.
This is very straight-forward and there aren’t any tricks. Just make sure that your Recipe model aligns with the given dummy data in this (the data type of your fields matter). Remember to use SDWebImage to download the images and to implement the correct function to handle tapping on a cell. You also need to figure out which labels will have multiple lines. Click on every single cell to check for edge cases.
Once you are done, stage, commit, and push to GitHub.
Your task is to create a horizontally scrolling collection view that represents the filter pills as well as adding filtering functionality.
Here is a quick demo of what we’re expecting:
The tricky part to this task is that there are now two collection views inside of this view controller. Since it’s not possible to create multiple cellForItemAt functions, for example, then you need to handle the logic within the function itself. You can do this with an if statement and checking to see if the parameter collectionView is equal to the collection view property.
You want the collection view to span over the entire screen’s width so the leading and trailing anchors need to equal to the parent view. To add an inset to the collection view’s content, you can configure the contentInset property.
There are 4 filters: All, Beginner, Intermediate, and Advanced. You can create an array of strings as the data model and use the string to configure the collection view cell which you can use a UIButton to represent.
If you want, you can disable the scroll indicator for a cleaner scrolling view.
You do not need to handle filter stacking. This is somewhat advanced so we will leave that for extra credit.
There are many ways to determine if a cell is selected, so I will leave this up to you to decide. Make sure that the currently selected tab is highlighted with a white text color. If you are lost and have no idea where to start, feel free to ask on Ed Discussion or come to office hours.
There are also many ways to change the value of the selected filter. You can configure didSelectItemAt or use delegation to communicate from the cell’s button to the view controller.
Once you are done, stage, commit, and push to GitHub.
✋🏻 This is the stopping point for the midpoint submission. We will grade you for completion based on your GitHub commit history.
Please submit a submission.txt file on CMSX similar to how you did it for your A3 midpoint submission. You can download the outline below and see an example submission right under it.
Your task is to create a GET request to fetch all recipes from this API:
You can use Postman to test the HTTP request. You will need to create a NetworkManager class with a shared singleton instance. You will be using Alamofire so make sure to import this library. See the lecture, textbook, or A3 for reference.
Error handling is not required but is nice to have. You will know if you integrated it correctly if there are more recipes than the dummy data. As a reminder, the JSON uses snake_case but Swift uses camelCase.
Networking is one of the most important but difficult concepts to learn and implement. We want you to get as much practice as you can to prepare you for the Hack Challenge. If you are confused, please create a post on Ed Discussion or visit office hours.
Make sure that filtering still works properly!
Once you are done, stage, commit, and push to GitHub.
Your task is to implement bookmarking functionality for these recipes. You will need a way to keep track of bookmarked recipes to save them locally via UserDefaults.
First, figure out what data structure you will use to keep track of bookmarked recipes. Then, think of a key that you will use to access through UserDefaults.
Recipes that are bookmarked should have a bookmark icon in their cell. See the Figma for UI details.
You will need to create a UIBarButtonItem to represent the bookmark button. This button will be in the detailed recipe view on the top right corner. If the recipe is already saved, the bookmark button will be filled and tapping on it will remove it from the saved recipes.
Here is a quick demo of what we are looking for:
Once you are done, stage, commit, and push to GitHub.
✋🏻 If you reach this point, you are done with the assignment. However, feel free to challenge yourself with the extra credit features.
Extra credit will only be given if the features are fully implemented. These are unordered and you can choose as many as you like.
Your task is to create a custom back button. The has a possible design for this, but you are free to use any button you like. This should be a freebie if you finished the task in Part V.
Right now, you can only select one filter at a time. Your task is to allow for filter stacking. All selected filters should be highlighted and the collection view should contain all selected filters.
Right now, you have two separate collection views: one for the filters and the other for the recipes. Because these collection views have different scrolling directions, if we wanted to make them both scrollable vertically, then we will have to nest collection views. Your task here is to nest collection views so that the filters scroll with the recipes. In other words, if I scroll up, the filters should scroll up as well while maintaining its horizontal scrolling attribute.
Your task here is to create a page listing out all bookmarked recipes. The design is up to your creativity, but there needs to be some way to push the detailed recipe view where you can then bookmark/unbookmark. You may also need to use delegation to update this bookmark list, similar to what you did in Part V.
Once you are done, stage, commit, and push to GitHub.
Double check that all of your files are properly pushed to GitHub.
Clone your repository into a separate folder on your local computer drive.
Run your project and make sure that your code does not crash and everything works as needed.
If you are satisfied, download this TXT file and fill it out. Make sure to use the
Confirm that your submission.txt is formatted like the following and submit it on .
Fill out this (worth 1 point).
git clone [email protected]:cs1998-601-FA25/NETID-a2.git
# Ex: git clone [email protected]:cs1998-601-fa23/vdb23-a2.gitgit clone [email protected]:cs1998-601-FA25/NETID1-NETID2-a2.gitName: Richie Sun
NetID: rs929
GitHub Repository: [email protected]:cs1998-601-SEM/NETID-a2.git
Extra Credit:
+1 : ____
+1 : ____How to work with Git and GitHub for version control
How to read documentation from outside resources
How to format and structure your code to follow MVC design pattern
How to follow common styling conventions used in industry
How to implement designs created on Figma
How to represent lists of data using a UICollectionView and a UICollectionViewCell
How to send GET requests to a backend API using Alamofire
How to write callbacks (completion handlers) to handle asynchronous calls
How to create a NetworkManager singleton class to contain network calls
How to decode a JSON using a JSONDecoder in Swift
How to handle errors with networking calls
_ / 1
F: Tapping on a Recipe cell pushes a detailed view
_ / 1
PART III: Filtering
_ / 3
UI: Collection view for filters WITH horizontal scrolling
_ / 1
UI: Selected filter is highlighted (separate from functionality)
_ / 1
F: Tapping on a filter filters the recipe data (one at at time; stacking filters is extra credit)
_ / 1
PART IV: Fetching Recipes
_ / 1
F: GET Request to Fetch Recipes
_ / 1
PART V: Bookmark Recipes
_ / 2
F: Bookmarking from the detailed view updates the collection view using delegation
_ / 1
F: Saved recipes are stored locally via UserDefaults (restart app to check)
_ / 1
OTHER
_ / 2
Feedback Survey
_ / 1
Styling: viewDidLoad calls helper functions
_ / 1
SUBTOTAL
_ / 15
EC: Custom back button
+ 1
EC: Stacking filters
+ 1
EC: Nesting collection views
+ 1
EC: Separate bookmark page
+ 2
Deduction: Crash Tax
-1 point
GRAND TOTAL
_ / 15 (+5)
alwaysBounceVertical = true.You do not have to worry about dynamic cell size. Set the text labels’ line limit to 2 lines and the height of the cell to around 216. The width, however, will depend on the size of the screen. Remember, we want to have two columns. Hint: We can multiply/divide the screen’s width by a certain factor.
Do not save recipe images in the Assets catalog. We will be using SDWebImage to download images from URLs.
You do not have to handle dynamic cell width. A height of 32 and width of 116 should work.
The filter collection view does not have to scroll vertically with the recipe collection view. This requires nesting collection views inside each other which is a very tedious process. You can have the recipe collection view cut off like this if scrolled:
To filter the array of recipes, you can use the filter higher order function. As a hint, you should have two properties containing the array of recipes: one for all recipes and another for filtered recipes.
The bookmark icon should change immediately on press. You will also need to use delegation to reload the recipe collection view so that the cells will be properly updated. Remember to use a weak reference!
All saved recipes should be stored locally. You can check by restarting the app. If the saved recipes do not reset, then you are good to go.
PART I: Recipe CollectionView
_ / 4
UI: Name, Image, Time, Rating
_ / 2
UI: 2 columns, Dynamic number of cells (adding a new item to the array creates a new item/cell)
_ / 1
UI: Each cell is unique and represents a different Recipe
_ / 1
PART II: Detailed Recipe View
_ / 3
UI: Image
_ / 1




UI: Name and Description
git clone [email protected]:cs1998-601-SEM/NETID-a4.git
# Ex: git clone [email protected]:cs1998-601-fa23/vdb23-a4.gitgit clone [email protected]:cs1998-601-SEM/NETID1-NETID2-a4.git// Given a UIImageView called `imageView` and a Recipe object with
// the property `imageUrl`
imageView.sd_setImage(with: URL(string: recipe.imageUrl))if collectionView == collectionViewOne {
// Do something here for collectionViewOne
} else if collectionView == collectionViewTwo {
// Do something here for collectionViewTwo
}Name: Richie Sun
NetID: rs929
GitHub Repository: [email protected]:cs1998-601-SEM/NETID-a3.git
Notes: I was having trouble with setting up the UICollectionViewDelegateFlowLayout...https://api.jsonbin.io/v3/b/64d033f18e4aa6225ecbcf9f?meta=falseName: Vin Bui
NetID: vdb23
GitHub Repository: [email protected]:cs1998-601-SEM/NETID-a4.git
Extra Credit:
+1 : ____
+1 : ____
+1 : ____
+1 : ____
+2 : ____Original Author: Vin Bui
Midpoint Due: Wednesday, November 5, 2025 11:59 pm Final Due: Monday, November 10, 2025 11:59 pm
In this assignment, you will be creating a “social media” app. You will be using Alamofire to send HTTP requests to a backend endpoint to fetch information.
Developer Skills
How to use Postman to test HTTP requests
How to read code written by other developers
How to read data received from the backend to structure frontend code
How to work with Git and GitHub for version control
Course Material
How to represent lists of data using a UICollectionView and a UICollectionViewCell
How to send GET requests to a backend API using Alamofire
How to send POST requests to a backend API using Alamofire
As with any other course at Cornell, the Code of Academic Integrity will be enforced in this class. All University-standard Academic Integrity guidelines should be followed. This includes proper attribution of any resources found online, including anything that may be open-sourced by AppDev. The University guidelines for Academic Integrity can be found .
This assignment can be done with ONE partner. You are also free to come to the instructors or any course staff for help. Programming forums like Stack Overflow or Hacking with Swift are allowed as long as you understand the code and are not copying it exactly. The majority of code (excluding external libraries) must be written by you or your partner. Code written through AI means such as ChatGPT is NOT ALLOWED. However, you may use these resources for assistance, although we highly encourage consulting Ed Discussion or office hours instead.
If you are stuck or need a bit of guidance, please make a post on Ed Discussion or visit . Please do not publicly post your code on Ed Discussion. If you are using an external resource such as Stack Overflow, keep in mind that we are using UIKit with Swift 5. If you see anything with @IBOutlet or any weird syntax, then you are most likely looking at a different version.
The feedback form link is located in the section of this handout.
UI: implements the user interface
F: implements the functionality
EC: extra credit
You are encouraged to use to test out HTTP requests. Please take a look at the .
Similar to A2, we will be using Figma for the design sketches. You can find the link to the Figma . If you do not have an account, you can create one under your Cornell email. If you need a refresher, check out the .
If you are having trouble understanding how we will be using Git in this course, please read the A1 handout under section, or visit office hours so we can assist you. As a reminder:
Stage: git add .
Commit: git commit -m "YOUR MESSAGE HERE"
Push: git push
Navigate to a folder on your device where you will keep all of your assignments. You can navigate to the folder using cd in Terminal.
Clone the repository on GitHub:
Replace NETID with your NetID
Replace SEM with the semester (such as fa23 or sp24)
If you have a partner, replace NETID1 and NETID2. Try changing the order if the former does not work.
If you are lost or getting any kind of error, create a post on Ed Discussion or come to office hours.
Navigate to the repository located on your local computer drive. Inside of the folder NETID-a3 should contain an Xcode project called A3.xcodeproj. Open up the project.
Once you have the project opened, on the left side of the screen you should see the Navigator which contains all of the folders and files in the directory. If not, press CMD + 0 (that’s a zero) on your keyboard. You should see something like this:
There is already code written in this file. As developers, we often build on top of what others have written which is why it is important that you practice this skill. You will often see code that you have never seen before, and it is your job to understand it.
FeedVC.swiftThis file contains the main view controller that you will be working with throughout the entire assignment. The “Create Post” cell has already been implemented but you will notice that you cannot see it. You will need to finish setting up the collection view. The lecture does not go over how to create different sections; however, the process is very similar to what we went over in lecture and we will guide you in this handout. There are TODO comments to help guide you.
CreatePostCollectionViewCell.swiftThis file represents the cell to create a post. You are free and encouraged to look over this file to help you implement your own custom collection view cell. You can also reference the lecture or textbook chapter . In addition, you will be asked to write code to send a network request to create a post. There is a TODO comment indicating where you should implement this logic.
NetworkManager.swiftThis file will contain the Alamofire code to send HTTP requests to the backend. Refer to the lectures or textbook chapters .
Date+Extension.swiftDO NOT EDIT THIS FILE! This file contains a function convertToAgo that returns a string representation of the Date object indicating how long ago this post was created. You will call this function on the property holding the post’s date when you create your custom collection view cell.
UIColor+Extension.swiftDO NOT EDIT THIS FILE! Similar to A2, this file contains colors that are featured in the design. To use the colors, simply type UIColor.a3.<color_name>. It is good practice to implement the design system before starting any project, making it very easy to use throughout the entire project. Look over this file to understand how it works and keep note of the colors available for you to use.
Throughout the provided files, you may have noticed the // MARK comments. These are used to keep the code organized.
Properties (View) are used for UIView objects such as UILabel, UIImageView, etc. You should mark these properties as private and make them constants (use let).
You are not limited to these sections and are free to add more (and you should). Because many of your data properties are marked as private, you may need to create an init function.
Follow these steps when implementing the UI:
Create the view
Initialize the view
Constrain the view
Run, confirm, and repeat
Your viewDidLoad method should contain mostly function calls to helper functions. We will be grading you on this.
Endpoint:
(EDIT 4/14 23:23: For liking and unliking a post, you'll have to pass in the post ID as a parameter of the URL)
UICollectionViewCellYour task is to create a custom UICollectionViewCell for the post. Create this file inside of the Views folder. You will need to create a struct or class (struct recommended) to represent a post. Create this file inside of the Models folder. As a reference, this is an example post object in JSON fetched from the backend.
You will need to figure out the name and type of your properties for this object. However, the time property will be a Date object (even though it’s a string in the JSON). I will show you how to decode this in Part III.
Because you have not implemented networking yet, you will need to create dummy data to test the UI. When creating these dummy data, you can use the code Date() for the time property. For the other fields, you can customize it however you like.
Your custom cell class will have the following:
Name (”Anonymous”)
Date
Image (AppDev Logo)
Post message body
Keep in mind the background color, text color, font style, corner radius, etc. You should already have practice in A2 implementing views so I will not guide you as much as A2. Feel free to Google or look at the CreatePostCollectionViewCell class as a reference. However, your custom cell class differs in that it will need a configure method. You can use the convertToAgo function for the date object and assign it to the label’s text to format the “time ago” string.
Once you are done, stage, commit, and push to GitHub.
UICollectionViewYour task is to create a UICollectionView representing the feed. There is already some code written that you will need to look over. There is also a custom cell class called CreatePostCollectionViewCell that represents the “Create Post” cell already implemented for you. You will need to register and use this cell along with the other custom cell you created in Part I.
This collection view contains 2 sections, each section containing different cell classes.
Similar to items, sections are zero-indexed meaning that the first section has index 0. Use this information to implement the functions required to conform to UICollectionViewDataSource, UICollectionViewDelegate, and UICollectionViewFlowLayoutDelegate.
When creating your FlowLayout, keep in mind the spacing between each item is 16px and between each section is 24px. To add the spacing between sections, implement the insetForSectionAt function in the
The first section contains only 1 cell and there is no data model associated with it. Again, the custom cell class for this section is CreatePostCollectionViewCell. I highly recommend that you read and understand the code written in this class. Once you are able to see this cell in your collection view, begin implementing the second section.
The second section contains a variable number of cells indicating that you will need a data model representing the posts. Because you have not implemented networking yet, you will need to create dummy data to test this. If you can see both sections and their cells, you should be good to go.
Note: For the scope of this course, we will not be handling self-sizing cells. The height for each cell is fixed and there are a maximum of three lines for the post message.
Once you are done, stage, commit, and push to GitHub.
✋🏻 This is the stopping point for the midpoint submission. We will grade you for completion based on your GitHub commit history.
Please submit a submission.txt file on CMSX similar to how you did it for your A2 submission. You can download the outline below and see an example submission right under it.
Your task is to send a GET request using Alamofire to fetch all posts from the backend. Currently, your posts are all hard-coded dummy data. Of course, we want to be able to receive posts created by other people, so we must integrate networking. In Part I, you were given an example JSON representing a post, and you created your model object based on this JSON. The reason for this is that it makes decoding the JSON received from the backend to your model very simple.
If you have not installed Postman yet, you can install it . Read on how to use Postman for this assignment. Then, add a new GET request with the URL: https://ios-course-backend.cornellappdev.com/api/posts/ . This should return a list of all posts from the backend with a 200 status code.
Your job is to integrate these posts into the frontend. You can decode the time field to a Date object if you set the decoder’s dateDecodingStrategy to .iso8601. For example:
Your callback (completion handler) will take in an array of Post objects ([Post]). It will also be very helpful to have proper error handling in your code. Refer to the lecture or textbook chapter .
Once you are able to fetch all posts from the backend, your next task is to add pull to refresh to your collection view. Follow these steps:
Once you are done, stage, commit, and push to GitHub.
Your task is to send a POST request using Alamofire to add a post to the backend.
A good rule of thumb is to always use Postman before writing the code.
Add a new request to your collection with a POST method
Enter the URL https://ios-course-backend.cornellappdev.com/api/posts/create/.
Click on the Body tab, select raw, and change the blue dropdown from “Text” to
If successful, the server returns a 201 status code with the above JSON data representing the post that was just created. You do not need to do anything with this information for this assignment, but it is a common practice for the backend to return this data. If you fetch all posts again, either through Postman or your app, you should see the new post that you created.
Similar to Part III, you will integrate this network call within your app. Follow these steps:
Create a function in NetworkManager that uses Alamofire to make the call. Remember that this is a POST request with a request body parameter called message. Proper error handling is highly recommended!
Call this function inside of CreatePostCollectionViewCell.createPost. There should be a TODO comment. As a hint, there is a text field in this class that you will need to use.
Once you are done, stage, commit, and push to GitHub.
Your task is to send a POST request using Alamofire to like a post.
Before you integrate networking, configure the like button to be filled with the color ruby if the post’s liked users contains your NetID.
Just like before, use Postman to test the backend call.
Add a new request to your collection with a POST method
Enter the URL: https://ios-course-backend.cornellappdev.com/api/posts/{postId}/like/ (EDIT 4/14 23:23: You'll have to pass in the post ID as a parameter of the URL)
Click on the Body tab, select
You will use your NetID (all lowercase). If the call is successful, you should receive the updated post.
There are many ways you can go about this. My recommendation for you is to pass a boolean to the callback to indicate whether or not the call was successful, similar to Part IV. If the call is successful, make the like button filled and increment the count by 1. Additionally, you should only be able to tap on the button if the button is not already filled red, so you will need to wrap your network request in an if statement.
You may notice that there is a delay before the button turns red when tapping on it. In apps like Instagram, usually the UI changes even if the API call fails. However, for the sake of simplicity and grading, we want the button to only turn red if the network call succeeds.
Once you are done, stage, commit, and push to GitHub.
✋🏻 If you reach this point, you are done with the assignment. However, feel free to challenge yourself with the extra credit features.
Extra credit will only be given if the features are fully implemented. These are unordered and you can choose as many as you like.
Your task is to send a POST request using Alamofire to unlike a post. This may seem similar to Part V, but it requires some additional frontend logic. When grading for this, we will unlike a post and refresh to make sure the backend is actually updated. If you try to unlike a post in which the given NetID does not already like it, you will get an error. You can test this out on Postman. The URL is https://ios-course-backend.cornellappdev.com/api/posts/{postId}/unlike/. (EDIT 4/14 23:23: You'll have to pass in the post ID as a parameter of the URL)
If you take a look at the file, you should see a design containing the text “Top” and “New”. Your task here is to sort the posts by the # of likes (top) and the most recent (new). For example, if the selected tab is “Top”, the post with the most likes will be at the top. If the selected tab is “New”, the most recent post will be at the top. Make sure that the color of the tab changes depending on what is selected.
Your task here is to add some animation when liking a post. You could add a scaling animation similar to most social media apps or do some other cool animation. As long as there is some animation when liking a post, you will get full credit.
Once you are done, stage, commit, and push to GitHub.
Double check that all of your files are properly pushed to GitHub.
Clone your repository into a separate folder on your local computer drive.
Run your project and make sure that your code does not crash and everything works as needed.
If you are satisfied, download this TXT file and fill it out. Make sure to use the
Confirm that your submission.txt is formatted like the following and submit it on .
Fill out this (worth 1 point).
Original project authored by Vin Bui, adapted to SwiftUI by Daniel Chuang
Midpoint Due: Monday November 17, 2025 11:59 pm Final Due: Tuesday November 25, 2025 11:59 pm
In this assignment, you will be creating a recipe book app. You will be able to fetch recipes from a server, filter by category, and save them for later!
Developer Skills
How to organize your project directory
How to use Postman to test HTTP requests
How to read data received from the backend to structure frontend code
How to work with Git and GitHub for version control
Course Material
How to set up multiple collection views and communicate between them in SwiftUI
How to filter data using higher order functions
How to save data locally using UserDefaults
As with any other course at Cornell, the Code of Academic Integrity will be enforced in this class. All University-standard Academic Integrity guidelines should be followed. This includes proper attribution of any resources found online, including anything that may be open-sourced by AppDev. The University guidelines for Academic Integrity can be found .
This assignment can be done with ONE partner. You are also free to come to the instructors or any course staff for help. Programming forums like Stack Overflow or Hacking with Swift are allowed as long as you understand the code and are not copying it exactly. The majority of code (excluding external libraries) must be written by you or your partner. Code written through AI means such as ChatGPT is NOT ALLOWED. However, you may use these resources for assistance, although we highly encourage consulting Ed Discussion or office hours instead.
If you are stuck or need a bit of guidance, please make a post on Ed Discussion or visit . Please do not publicly post your code on Ed Discussion.
The feedback form link is located in the section of this handout.
UI: implements the user interface
F: implements the functionality
EC: extra credit
You are encouraged to use to test out HTTP requests. Please take a look at the .
Similar to A2 and A3, we will be using Figma for the design sketches. You can find the link to the Figma . If you do not have an account, you can create one under your Cornell email. If you need a refresher, check out the .
Go to Xcode -> File -> New -> Project -> App -> [MAKE SURE THAT Interface = SwiftUI] and just proceed from there via clicking Next and Create.
If you are having trouble understanding how we will be using Git in this course, please read the A1 handout under section, or visit office hours so we can assist you. As a reminder:
Stage: git add .
Commit: git commit -m "YOUR MESSAGE HERE"
Push: git push
Navigate to a folder on your device where you will keep all of your assignments. You can navigate to the folder using cd in Terminal.
Clone the repository on GitHub:
Replace NETID with your NetID
Replace SEM with the semester (such as fa23 or sp24)
If you have a partner, replace NETID1 and NETID2. Try changing the order if the former does not work.
If you are lost or getting any kind of error, create a post on Ed Discussion or come to office hours.
Color File
You may find the following code helpful to put into a Color.swift file, as it will allow for you to input the hex values of colors you have from Figma into Swift directly.
You will now be able to make colors via the following syntax. Note that the "0x" at the beginning is telling Swift that the proceeding values should be interpreted in hexadecimal base.
You will need to import Alamofire. Please import this via Cocoapods or SwiftPackageManager (SPM). You should be able to figure out how to install via Cocoapods via previous projects, but for SPM, here are the instructions!
XCode -> File -> Add Package Dependencies
Look up Alamofire -> Add Package
Click on the Project Settings Page (the very first icon in your file navigation bar, on the top left)
Click on General on the top bar of the newly opened project settings page
Notice that on Figma, the screens are broken down into the different stages you need to implement.
Your first task is to create a "Collection View" to display the recipes. We encourage using the textbook and the internet for syntax and modifier help! We will not guide you as much as we did with the other assignments, but here is a general blueprint for what to do:
Set up your Recipe struct and dummy data using data from this link. Make sure the struct's properties align with the Pastebin format!
Make a RecipeCell view in a new file called RecipeCell.swift
Set up a preview if you'd like!
Side notes:
You are not required to implement the bookmark icon until Part V, but you are free to do so now.
You will not be implementing the filters until Part III.
You do not have to worry about dynamic cell size. Set the text labels’ line limit to 2 lines and the height of the cell to around 216. The width, however, will depend on the size of the screen. Remember, we want to have two columns. Hint: We can multiply/divide the screen’s width by a certain factor.
Once you are done, stage, commit, and push to GitHub.
Your task is to create a view controller representing a detailed recipe view. You will push this view controller when tapping on the collection view cell. This detailed view will be unique to the recipe.
Detailed Recipe Page
This is very straight-forward and there aren’t any tricks. Make a new view called RecipePage.swift and implement what you see on Figma. Make sure that recipe is a property of the view (like before) so that you can input the recipe you want as a parameter.
Remember to use AsyncImage to download the images (feel free to copy and paste).
Navigating to the Detailed Recipe Page
This is super simple: first, wrap your ENTIRE ContentView in a NavigationView. Then, wrap each of your RecipeCells in the ForEach from before with NavigationLink, with a destination parameter of RecipePage(recipe: recipe). Feel free to refer back to SwiftUI II's lecture for a code snippet on this.
Once you are done, stage, commit, and push to GitHub.
Your task is to create a horizontally scrolling collection view that represents the filter pills as well as adding filtering functionality.
Here is a quick demo of what we’re expecting:
In UIKit, this would be very difficult. Thankfully for you, you're coding in SwiftUI!
Make a selectedDifficulty property in ContentView and make a difficulties property in ContentView. Feel free to copy this in, as long as you understand why we will need the @State. Your default selectedDifficulty should be "All"
Make a ScrollView containing a HStack between the ChefOS title and your recipe cells. The following shows how to allow for horizontal scrolling.
Make a ForEach in this HStack that looks through difficulties. You will need to set it up with the id parameter as well, since difficulties doesn't conform to identifiable.
Make these little capsule filter buttons inside the for each. One way to do this is by making each of them a Button. The code / function that the Button actually calls should be as easy as setting selectedDifficulty = Difficulty. Then, make the label a Text component that uses the .background() modifier with a capsule inside
You do not need to handle filter stacking. This is somewhat advanced so we will leave that for extra credit.
There are many ways to determine if a cell is selected, so I will leave this up to you to decide. Make sure that the currently selected tab is highlighted with a white text color. If you are lost and have no idea where to start, feel free to ask on Ed Discussion or come to office hours.
To filter the array of recipes, you can use the filter higher order function. Feel free to look up documentation for this, or just check out A1.
Once you are done, stage, commit, and push to GitHub.
✋🏻 This is the stopping point for the midpoint submission. We will grade you for completion based on your GitHub commit history.
Please submit a submission.txt file on CMSX similar to how you did it for your A3 midpoint submission. You can download the outline below and see an example submission right under it.
Your task is to create a GET request to fetch all recipes from this API:
You can use Postman to test the HTTP request. You will need to create a NetworkManager class with a shared singleton instance. You will be using Alamofire so make sure to import this library. See the lecture, textbook, or A3 for reference.
Error handling is not required but is nice to have. You will know if you integrated it correctly if there are more recipes than the dummy data. As a reminder, the JSON uses snake_case but Swift uses camelCase.
Networking is one of the most important but difficult concepts to learn and implement. We want you to get as much practice as you can to prepare you for the Hack Challenge. If you are confused, please create a post on Ed Discussion or visit office hours.
One caveat for SwiftUI is that you will need to call your fetch function (e.g. NetworkManager.fetchRecipes()) inside of a .onAppear modifier instead of viewDidLoad() like in UIKit
You'll need to update your model for recipe to make ID into a UUID instead of a string, like this:
Next, add coding keys into your recipe struct:
Make two init functions for the recipe struct, one that is a default initializer for your dummy data, and another is a networking initializer for fetching. This is how you should do the networking initializer:
Finally, check that your networking is actually working. Make sure you see King Pao Chicken in your recipes in the app - that is a recipe that is in the endpoint but not the dummy data.
Make sure that filtering still works properly!
Once you are done, stage, commit, and push to GitHub.
Your task is to implement bookmarking functionality for these recipes. You will need a way to keep track of bookmarked recipes to save them locally via UserDefaults.
First, figure out what data structure you will use to keep track of bookmarked recipes. Then, think of a key that you will use to access through UserDefaults. My recommendation is that you use a BookmarkManager that is a singleton instance.
In your BookmarkManager, you'll want to include:
@Published bookmarkRecipesIds, which is a Set<UUID> (set of UUIDs from your recipe ids).
Here is a quick demo of what we are looking for:
Once you are done, stage, commit, and push to GitHub.
✋🏻 If you reach this point, you are done with the assignment. However, feel free to challenge yourself with the extra credit features.
Extra credit will only be given if the features are fully implemented. These are unordered and you can choose as many as you like.
Your task is to create a custom back button. The has a possible design for this, but you are free to use any button you like. This should be a freebie if you finished the task in Part V.
Right now, you can only select one filter at a time. Your task is to allow for filter stacking. All selected filters should be highlighted and the collection view should contain all selected filters.
Right now, you have two separate collection views: one for the filters and the other for the recipes. Because these collection views have different scrolling directions, if we wanted to make them both scrollable vertically, then we will have to nest collection views. Your task here is to nest collection views so that the filters scroll with the recipes. In other words, if I scroll up, the filters should scroll up as well while maintaining its horizontal scrolling attribute.
Your task here is to create a page listing out all bookmarked recipes. The design is up to your creativity, but there needs to be some way to push the detailed recipe view where you can then bookmark/unbookmark. You may also need to use delegation to update this bookmark list, similar to what you did in Part V.
Once you are done, stage, commit, and push to GitHub.
Double check that all of your files are properly pushed to GitHub.
Clone your repository into a separate folder on your local computer drive.
Run your project and make sure that your code does not crash and everything works as needed.
If you are satisfied, download this TXT file and fill it out. Make sure to use the
Confirm that your submission.txt is formatted like the following and submit it on .
Fill out this (worth 1 point).
How to read documentation from outside resources
How to format and structure your code to follow MVC design pattern
How to follow common styling conventions used in industry
How to implement designs created on Figma
How to write callbacks (completion handlers) to handle asynchronous calls
How to create a NetworkManager singleton class to contain network calls
How to decode a JSON using a JSONDecoder in Swift
How to handle errors with networking calls
_ / 1
PART III: Fetching Posts
_ / 3
F: GET Request to Fetch Posts
_ / 2
F: Refresh Control
_ / 1
PART IV: Creating a Post
_ / 3
F: POST Request to Create a Post
_ / 3
PART V: Liking a Post
_ / 2
F: POST Request to Like a Post
_ / 1
F: ❤️ turns red if liked, # likes goes up
_ / 1
OTHER
_ / 2
Feedback Survey
_ / 1
Styling: viewDidLoad calls helper functions
_ / 1
SUBTOTAL
_ / 15
EC: POST Request to Unlike a Post
+ 1
EC: Sort by Top/New posts
+ 1
EC: Animation when liking a Post
+ 1
Deduction: Crash Tax
-1 point
GRAND TOTAL
_ / 15 (+3)
Properties (Data) are used for data types such as String, Int, delegates, etc. Again, mark these properties as private but it is up to you to decide if they are constants or variables.
The Set Up Views section should be used for initializing your view properties.
/api/posts/{postId}/like/
netId (String)
Unlike a post
POST
/api/posts/{postId}/unlike/
netId (String)
Like button (use non-filled heart for now)
Number of likes
JSONThis request expects the following body:
(Optional) If the call is successful, clear the the textfield. You can pass true to the callback if successful or false otherwise.
Run the app and try to create a post. For the scope of this assignment, you do not need to have the collection view updated as soon as you create the post. However, refreshing the collection view should contain the new post.
JSONThis request expects the following body:
PART I: Creating the UICollectionViewCell
_ / 2
UI: Header (name, date, image)
_ / 1
UI: Post Message, Like Button, # Likes
_ / 1
PART II: Creating the UICollectionView
_ / 3
UI: Multiple sections
_ / 1
UI: Dynamic number of items/cells (adding a new Post to the array adds a new item/cell)
_ / 1
Fetch all posts
GET
/api/posts/
None
Create a post
POST
/api/posts/create/
message (String)
Like a post



UI: Each cell is unique and represents a different Post
POST
How to read documentation from outside resources
How to format and structure your code to follow MVC design pattern
How to follow common styling conventions used in industry
How to implement designs created on Figma
How to represent lists of data using a UICollectionView and a UICollectionViewCell
How to send GET requests to a backend API using Alamofire
How to write callbacks (completion handlers) to handle asynchronous calls
How to create a NetworkManager singleton class to contain network calls
How to decode a JSON using a JSONDecoder in Swift
How to handle errors with networking calls
_ / 1
F: Tapping on a Recipe cell pushes a detailed view
_ / 1
PART III: Filtering
_ / 3
UI: Collection view for filters WITH horizontal scrolling
_ / 1
UI: Selected filter is highlighted (separate from functionality)
_ / 1
F: Tapping on a filter filters the recipe data (one at at time; stacking filters is extra credit)
_ / 1
PART IV: Fetching Recipes
_ / 1
F: GET Request to Fetch Recipes
_ / 1
PART V: Bookmark Recipes
_ / 2
F: Bookmarking from the detailed view updates the collection view using delegation
_ / 1
F: Saved recipes are stored locally via UserDefaults (restart app to check)
_ / 1
OTHER
_ / 2
Feedback Survey
_ / 1
onAppear calls networking functions
_ / 1
SUBTOTAL
_ / 15
EC: Custom back button
+ 1
EC: Stacking filters
+ 1
EC: Nesting collection views
+ 1
EC: Separate bookmark page
+ 2
Deduction: Crash Tax
-1 point
GRAND TOTAL
_ / 15 (+5)
Scroll down to Frameworks, Libraries, and Embedded Content and add Alamofire.
You're ready to import Alamofire into your code and write some solid networking code!
recipe which should allow for you to input any recipe you want from your dummy data (so your string should be using recipe.[property])Use an AsyncImage for putting in the recipe's image. Refer to Swift documentation or the Ed post made by Daniel for a reference on how to do this.
Now, in ContentView, you'll want to set up a LazyVGrid in order to get the grid with 2 columns. Refer to the Swift documentation for this!
Unlike a normal VStack, we need to initialize columns and input that as a parameter, so make sure to do that
After that, just write a ForEach inside of the LazyVGrid. You might need to make sure that your Recipe struct conforms to certain things before you can proceed with this, but XCode should tell you exactly what you need to do in this regard!
Wrap your LazyVGrid with a ScrollView
Wrap your LazyVGrid + ScrollView component in a VStack, and add some text right above it in the VStack that says "ChefOS" just how the Figma has it (so the scroll only scrolls through the items, not the ChefOS title)
Make your your styling matches the Figma!
Do not save recipe images in the Assets catalog. You must use AsyncImage for displaying these images
loadBookmarks() function that loads from UserDefaults
toggleBookmarks() function that adds or removes the ID from bookmarkRecipeIds, and calls saveBookmarks afterwards
an init function that calls loadBookmarks()
Then, in ContentView, RecipePage, and RecipeCell, set up an @StateObject called bookmarkManager that is equal to BookmarkManager.shared. If you don't make it a State object and instead use BookmarkManager.shared directly, your bookmark icons will not be updated!
You'll need to use the .onAppear{} modifier on your views to make sure that the view gets the data from BookmarkManager.
Update the UI so that recipes that are bookmarked should have a bookmark icon in their cell. See the Figma for UI details. See the point below on how to get the bookmark icon.
You will need to create a ToolbarItem to represent the bookmark button in RecipePage. This button will be in the detailed recipe view on the top right corner. If the recipe is already saved, the bookmark button will be filled and tapping on it will remove it from the saved recipes. You can add this to your RecipePage by using the .toolbar{}modifier at the end of the RecipePage and putting ToolbarItem wrapping a Button wrapping Image(systemName: "bookmark") or Image(systemName: "bookmark.fill")
Adding the bookmark to the RecipeCell should be as easy as just putting the bookmark image in an if statement.
All saved recipes should be stored locally. You can check by restarting the app. If the saved recipes do not reset, then you are good to go.
PART I: Recipe CollectionView
_ / 4
UI: Name, Image, Time, Rating
_ / 2
UI: 2 columns, Dynamic number of cells (adding a new item to the array creates a new item/cell)
_ / 1
UI: Each cell is unique and represents a different Recipe
_ / 1
PART II: Detailed Recipe View
_ / 3
UI: Image
_ / 1

UI: Name and Description
git clone [email protected]:cs1998-601-SEM/NETID-a3.git
# Ex: git clone [email protected]:cs1998-601-fa23/vdb23-a3.gitgit clone [email protected]:cs1998-601-SEM/NETID1-NETID2-a3.git{
"id": "7m03J198pyXFBvCOLNbw",
"likes": ["vdb23", "rs929"],
"messsage": "Howdy!",
"time": "2023-06-12T22:03:23Z"
}Name: Richie Sun
NetID: rs929
GitHub Repository: [email protected]:cs1998-601-SEM/NETID-a3.git
Notes: I was having trouble with setting up the UICollectionViewDelegateFlowLayout...let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601// 1. Create a UIRefreshControl view property
private let refreshControl = UIRefreshControl()
// 2. Add a function to be called as a target
refreshControl.addTarget(self, action: #selector(fetchAllPosts), for: .valueChanged)
// 3. Assign the collection view’s refresh control
collectionView.refreshControl = refreshControl
// 4. Stop refreshing after the network call is complete
refreshControl.endRefreshing(){
"message": "<Enter some message here>"
}{
"netId": "<Enter your NetID>"
}Name: Vin Bui
NetID: vdb23
GitHub Repository: [email protected]:cs1998-601-fa23/vdb23-a3.git
Extra Credit:
+1 : ____
+1 : ____
+1 : ____git init
git remote add origin [email protected]:cs1998-601-SEM/NETID-a4.git
# Ex: git remote add origin [email protected]:cs1998-601-fa23/vdb23-a4.git
git add .
git commit -m "Initial commit"
git push origin maingit clone [email protected]:cs1998-601-SEM/NETID1-NETID2-a4.git// Copy into a file called Color.swift
import SwiftUI
extension Color {
init(hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 08) & 0xff) / 255,
blue: Double((hex >> 00) & 0xff) / 255,
opacity: alpha
)
}
}Color(hex: 0xFAFAFA)@State private var selectedDifficulty = "All"
private let difficulties: [String] = ["All", "Beginner", "Intermediate", "Advanced"]ScrollView(.horizontal, showsIndicators: false)ForEach(difficulties, id: \.self) {
...
}Text(difficulty)
// fonts and other modifiers
.background( Capsule()
// more modifiers here for the Capsule specifically
)Name: Richie Sun
NetID: rs929
GitHub Repository: [email protected]:cs1998-601-SEM/NETID-a3.git
Notes: I was having trouble with setting up the UICollectionViewDelegateFlowLayout...https://api.jsonbin.io/v3/b/64d033f18e4aa6225ecbcf9f?meta=falsevar id: UUID?enum CodingKeys: String, CodingKey {
case id, description, difficulty, imageUrl = "image_url", name, rating
}init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Convert string ID to UUID
if let idString = try container.decodeIfPresent(String.self, forKey: .id) {
self.id = UUID(uuidString: idString)
} else {
self.id = nil
}
// Decode the rest of the properties normally
self.description = try container.decode(String.self, forKey: .description)
self.difficulty = try container.decode(String.self, forKey: .difficulty)
self.imageUrl = try container.decode(String.self, forKey: .imageUrl)
self.name = try container.decode(String.self, forKey: .name)
self.rating = try container.decode(Float.self, forKey: .rating)
}Name: Vin Bui
NetID: vdb23
GitHub Repository: [email protected]:cs1998-601-SEM/NETID-a4.git
Extra Credit:
+1 : ____
+1 : ____
+1 : ____
+1 : ____
+2 : ____