CIS 1951
Fall 2024

HW3: Penn Dining Scavenger Hunt

This assignment can be completed individually or in pairs. You can use Git to collaborate if you'd like.

Author: Anthony Li

Please leave feedback by posting on Ed or contacting the course staff.

Required Software: macOS Ventura, Xcode 15

Deadline: Thursday, 10/31 @ 11:59 PM

Download GPX Files ↓ or view on GitHub

Introduction

In this assignment, you'll be building a location-based scavenger hunt game centered around the food and beverage institution we all know and love: Penn Dining.

You'll use the Core Location framework to detect when the user is near a dining hall. Then, you'll use either the Core Motion framework or SwiftUI's gesture support to allow the user to "collect" a dining hall by shaking their device or scribbling on the screen.

This assignment doesn't have any starter code -- the GPX files are there to help you test.

Requirements

Your game should have two screens:

  • A home screen, showing a list of dining halls and whether they've been "collected"
    • Tapping on a dining hall should take the user to the dining hall screen for the corresponding hall
  • A dining hall screen that allows a user to "collect" a dining hall

Specifically, the dining hall screen should:

  1. Check if the dining hall has already been collected. If it has, it should display a message saying so and prevent the user from collecting it again.
  2. Check if the user is within 50 meters of the dining hall. If they are not, it should display an error and prevent the user from collecting the dining hall.
  3. Allow the user to collect the dining hall through one of two methods:
    • Shaking the device: Use the Core Motion framework to collect the dining hall the user shakes the device
      • You must use Core Motion to detect the shake with accelerometer data. Using UIWindow.motionEnded() will lead to reduced credit.
      • The algorithm doesn't need to be perfect (there can be some false positives) - just make sure ordinary motion won't trigger the collection.
    • Drawing a scribble: Use SwiftUI's gesture support to collect the dining hall when the user scribbles on the screen back and forth

We recommend going with the shaking method as it's more fun, but you should choose the scribble method if you can't test on a physical device (e.g. you don't have a Mac or an iPhone). Do not implement both methods - you will only receive credit for one.

You should keep track of the list of collected dining halls in a view model that is shared by both screens. Each dining hall should be represented by a model struct.

Your game should contain the following dining halls:

  • 1920 Commons
  • Accenture Café
  • Falk Kosher Dining
  • Hill House
  • Houston Market
  • Joe's Café
  • Kings Court English House
  • Lauder College House
  • McClelland Express
  • Pret A Manger
  • Quaker Kitchen

Coordinates for these can be found in the GPX files.

NOTE: You can just hardcode the coordinates in your app's code with our Locations.swift file!

Finally, please submit a short, 5-second video of a dining hall being collected along with your project. This will help us grade, just in case we're not sure how to trigger the collection event.

Note: You are not required to store the user's progress between app launches. We will also not prescribe a specific way you should handle errors.

Simulating Location

If you don't have access to a physical device or don't feel like making the trek to each of the dining halls, worry not! Xcode lets you simulate your device's location using a GPX file. To use it:

  1. Download the GPX files from the link above.
  2. Run your app in the simulator or on a device. Your app must be actively running outside of the Xcode preview before you can continue.
  3. In Xcode's menu bar, go to Debug > Simulate Location > Add GPX File to Project... and select the GPX files for each of the dining halls. (You can shift-click to select multiple files at once.)
    • If this option is not enabled, stop your app and try running it again.
  4. Once you've added the GPX files, you can select them from the Simulate Location menu to simulate your device's location.

Note that if you're running on a physical device, location simulation will only work while your app is actively being run from Xcode. Running your app from the home screen will use the device's actual location.

(You can also use the Features > Location menu in the Simulator, but doing it through Xcode will let you use our GPX files.)

Instructions

Some brief instructions to get you started:

Step 1: Plan your app

Start by reading through these instructions and planning out how you're going to structure your app and its screens.

In particular, think through:

  • What models, view models, and views do you need?
  • How will your view model communicate with its views?
  • Which part of your architecture will handle location, motion, and gesture data?
  • How and where will you keep track of which dining halls have been collected?
  • If you're using motion, how will you detect a shake?
  • If you're using gestures, how will you detect a scribble?

Step 2: Model each dining hall using a struct

When you're ready, create a new SwiftUI project. You'll most likely want to start by modeling each dining hall using a struct.

You'll want to keep track of a dining hall's name, its location, and maybe some other properties (such as an ID to make it conform to Identifiable). Here's a starting point for your dining hall struct:

struct DiningHall: Identifiable {
    var id: UUID
    var name: String
    var location: CLLocation
}

CLLocation represents a location -- you can use it to represent the location of each dining hall. (Don't worry about finding the altitude of each dining hall.)

Once you've made this struct, you'll want to create an array of dining halls somewhere in the app. You can hardcode this array using our provided coordinates.

Step 3: Create a view model to store collected dining halls

Now, you'll want to create a view model to keep track of which dining halls have been collected. Ideally, this view model should be shared between the home and dining hall screens. Declare a new ObservableObject, like this:

class DiningHallViewModel: ObservableObject {
    // Add a @Published property to keep track of which dining halls have been collected

    // Add methods, such as collecting and checking if a dining hall has been collected
}

You can then use @StateObject in your App struct to create an instance of this view model for your app. Then, use the .environmentObject modifier to pass it to your home and dining hall screens, much like we did in lecture 5.

Step 4: Write the home screen

Now, you can start writing the home screen, using the view model and models you wrote in steps 2 and 3. You can use a simple List view for this.

Step 5: Begin writing the dining hall screen

The dining hall screen is where most of the action happens. This is a pretty complicated screen, so we recommend you split out the dirty logic into a separate view model.

You'll most likely want to start with the first check: seeing if a dining hall has already been collected using the view model you wrote earlier.

Step 6: Set up your CLLocationManager and its delegate

It's time to start using the Core Location framework. Set up a CLLocationManager, and wire up one of your objects to be its delegate. To help you with this, you can consult our finished code from lecture 7, or Apple's Configuring Your App to Use Location Services tutorial.

Once you've set up your manager and delegate, go ahead and request location permissions from the user, being sure to handle the case where the user has already approved your request. Be sure to set up a purpose string!

Step 7: Check whether the user is within 50 meters of a dining hall

Now that you've obtained location permissions, you can start checking if the user is within 50 meters of a given dining hall. To do this:

  1. Request the user's location by calling the requestLocation() method.
  2. When you receive the location in the delegate, use the distance(from:) method to check how far the user is from the dining hall.
  3. Update your UI and state based on the result.

To test your app, you can use the location simulation steps above!

Step 8: Implement the logic to collect a dining hall

With location checking out of the way, you can now implement the logic to collect a dining hall, using either the Core Motion framework or SwiftUI's gesture support.

If you're using motion: You'll need to set up a CMMotionManager object, listen for device motion updates, then use the accelerometer data to detect a shake. For help with setting up access to motion data, you can consult our finished code from lecture 7.

If you're using gestures: You'll need to set up a gesture on your view, then use the position data you get from the onChanged handler to determine whether the user is scribbling. You may want to add a few state variables to help you with this. For help with gesture recognition, you can check out the slides from lecture 6 or our finished code from lecture 6.

Step 9: Test and submit!

Be sure to test collecting multiple dining halls in a run. Don't forget to record a short video of a dining hall being collected!

Resources

This is a fairly complex assignment. We encourage you to come to office hours or ask on Ed if you have any questions or are running into trouble. We're here to help!

Some relevant course material:

Grading

This assignment is worth 100 points, broken down as follows:

Home and Dining Hall Screens (20 points)

  • 5 points: Home screen displays a list of dining halls
  • 5 points: Home screen displays whether each dining hall has been collected
  • 5 points: Tapping on a dining hall takes the user to the dining hall screen for that hall
  • 5 points: Dining hall screen checks if the dining hall has already been collected

Location (25 points)

  • 5 points: App sets up a CLLocationManager and its delegate
  • 3 points: App requests location permissions
  • 2 points: App has a purpose string for location permissions that clearly explains why the app needs the user's location
  • 15 points: Dining hall screen checks if the user is within 50 meters of the dining hall

Collection (25 points)

If you're using Core Motion:

  • 5 points: App sets up a CMMotionManager object and listens for device motion updates
  • 20 points: Dining hall screen allows the user to collect the dining hall by shaking the device

If you're using SwiftUI gestures:

  • 5 points: Dining hall screen sets up a gesture on the view
  • 20 points: Dining hall screen allows the user to collect the dining hall by drawing a scribble

App Architecture (20 points)

  • 5 points: App uses a model struct to represent each dining hall
  • 10 points: App uses a view model to keep track of which dining halls have been collected
  • 5 points: App successfully uses @StateObject, @ObservedObject, and/or @EnvironmentObject to share the view model between the home and dining hall screens
    • If you're using @Observable, you can also use @State, @Bindable, and/or @Environment

Video (5 points)

  • 5 points: A short video is submitted showing a dining hall being collected

Code Quality (5 points)

  • 5 points: Code is readable (i.e. indentation and naming of symbols is reasonable)

Deductions

  • -5 points: Xcode project contains a broken file reference preventing compilation
  • -10 points: Gesture/motion detection is unreliable (e.g. egregious false positives or negatives)
  • -20 points: App doesn't compile

General bugs (those not covered in the above deductions) will be subject to these deductions:

  • -2 points: Minor bug, e.g. a UI/UX issue that appears in limited circumstances and doesn't affect the core flow
  • -5 points: Major bug, e.g. a bug that prevents the core flow of the app but can be worked around in-app
  • -10 points: Fatal bug, e.g. a bug that requires code modification to continue testing the core flow
    • Crashes on launch, or during major parts of the app flow, will fall into this category

Bugs encountered when testing extra credits will be deducted from their respective extra credit points.

Extra Credits

  • Map (+5): Instead of a list, display the dining halls on a map. The user should be able to tap on a dining hall to go to the dining hall screen. The map should also show the user's current location.
  • Permission Priming (+5): Before requesting location permissions, use the .sheet modifier to display a message explaining why the app needs the user's location.
    • Setting a purpose string is insufficient for this extra credit - you must display a separate message to the user.

Submission

Before you submit, make sure you:

  1. Give the requirements another read
  2. Make sure you've implemented all the requirements
  3. Compile and test your app in the simulator or on a device (not just the SwiftUI preview)

Once you're done with those steps, upload a zip (or .tar.gz) of your entire Xcode project, including GPX files (if any) and video to Gradescope. If you're submitting in pairs, have one person submit and add the other person as a group member.

Submit on Gradescope →
Dates and times are displayed in EST.