Tutorial: Maps SwiftUI

Cover Page

DUE Wed, 10/8, 2 pm

The goals of this tutorial are threefold: first, to introduce you to on-device asynchronous event stream in the form of Swift’s AsyncSequence and the AsyncStream generator. Second, to introduce you to Apple’s CoreLocation API, which is part of the MapKit SDK. And third, to implement navigation between multiple screens, including the use of swipe gesture for navigation. We will build on the code base from the second tutorial, Chatter.

Reading the magnetometer part of this tutorial requires access to a physical device. While the iPhone simulator can simulate location data, it cannot simulate the magnetometer used to determine facing.

This tutorial may be completed individually or in teams of at most 2. You can partner differently for each tutorial.

Expected behavior

In the map-augmented Chatter app, we will add a Map View. On the map, there will be one or more markers. Each marker represents a posted chatt. If you click on a marker, it displays the poster’s username, message, timestamp, and their geodata, consisting of their geolocation and velocity (compass-point facing and movement speed), captured at the time the chatt was posted. If a chatt was posted with the user’s geodata, the timeline now shows the chatt with a pin. Clicking this pin brings user to the MapView with the chatt’s posted location marked on the map.

We will also implement a swiping gesture to allow users to switch from the main timeline view to the map view. When a user swipes left to transition from the timeline view to the map view, each retrieved chatts will be displayed as an individual marker on the map. From the map view, users can not post a chatt; they can only return to the timeline view using the back arrow. User also cannot initiate a new retrieval of chatts in the map view.

Post a new chatt and view chatts on MapView:

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

Preparing your GitHub repo

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

  reactive
    |-- chatterd
    |-- chatterd.crt
    |-- maps
        |-- swiftUIChatter
            |-- swiftUIChatter.xcodeproj
            |-- swiftUIChatter
    # and other files or folders

YOUR*TUTORIALS folder on your laptop should contain the chatter.zip file in addition.

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

Working with location data as an asynchronous stream

We first look at how to get user’s geolocation information (latitude (lat), longitude (lon), and velocity data (facing and speed)).

We can get continuous updates of the device’s location and heading from Apple’s CoreLocation API. The location updates are delivered as an AsyncSequence stream. The heading updates API, however, still relies on delegated callback: we have to provide a function that CoreLocation calls whenever there’s any heading update. We will create an AsyncStream that takes the delivered headings to feed an AsyncSequence stream.

Requesting permission

To get device’s location, we must first request user’s permission and explain why we need the location data. The request for permission is accomplished by providing justification-for-access to the Info list. Click on your project name (first item in your left/navigator pane), then click on the project in the TARGETS section, and then the Info tab. In the Custom iOS Target Properties section, right click (or ctl-click) on any row in the table and choose Add Row (screenshot). Select Privacy - Location When In Use Usage Description (you can type to match search) and in the Value field to the right enter the reason you want to access location, for example, “to post chatt poster’s location”. What you enter into the value field will be displayed to the user when seeking their permission . On app’s launch, we will prompt the user for permission to access the device’s location, but let’s implement the location manager first.

If you accidentally choose “Allow Once” or “Don’t Allow” when your app requests permission, go to Settings > Privacy > Location Services, select your app and tap “Ask Next Time” to reset it.

Location Manager

Create a Swift file and name it LocManager. At the top of the file, import MapKit and Observation and create the following Location struct:

import MapKit
import Observation

struct Location {
    var lat: CLLocationDegrees
    var lon: CLLocationDegrees
    var speed: CLLocationSpeed
}

We will store information from MapKit’s legacy CLHeading class in this struct so that we can pass it between tasks without the compiler complaining that CLHeading is not Sendable.

We now create an observable LocManagerViewModel singleton that SwiftUI can observe and react to:

@Observable
final class LocManagerViewModel {
    static let shared = LocManagerViewModel()
    private init() {}
    
    private(set) var location = Location(lat: 0.0, lon: 0.0, speed: 0.0)

    @ObservationIgnored
    var speed: String {
        switch location.speed {
            case 0.5..<5: "walking"
            case 5..<7: "running"
            case 7..<13: "cycling"
            case 13..<90: "driving"
            case 90..<139: "in train"
            case 139..<225: "flying"
            default: "resting"
        }
    }

    func setLocation(lat: CLLocationDegrees, lon: CLLocationDegrees, speed: CLLocationSpeed) {
        location.lat = lat
        location.lon = lon
        location.speed = speed
    }
    
    private(set) var heading: CLLocationDirection? = nil
    private let compass = ["North", "NE", "East", "SE", "South", "SW", "West", "NW", "North"]
    var compassHeading: String {
        return if let heading {
            compass[Int(round(heading.truncatingRemainder(dividingBy: 360) / 45))]
        } else {
            "unknown"
        }
    }
    
    func setHeading(_ newHeading: CLLocationDirection?) {
        heading = newHeading
    }
}

The stored property location holds the latest location reported by the device. With each location update, we also get a speed update from the device. The computed property speed returns a descriptive text of the latest movement speed. The stored property heading holds the latest heading reported by the device. The computed property compassHeading returns the heading in a human-friendly compass reading.

For thread safety, we would ideally create an actor to host the location and heading states. But the CLLocationManagerDelegate protocol and CLLocationManager class being legacy API make it simpler to host them in a class instead:

final class LocManager: NSObject, CLLocationManagerDelegate {
    static let shared = LocManager()
    private let locManager = CLLocationManager()
    
    override private init() {
        super.init()

        // configure the location manager
        locManager.desiredAccuracy = kCLLocationAccuracyBest
        locManager.delegate = self
    }

    // start updates
}

Inside the class we create an instance of CLLocationManager, specify our desired location accuracy, and assign self as the delegate to handle CLLocationManagerDelegate callbacks. To be the delegate, the class must adopt CLLocationManagerDelegate, which requires that the class must be a subtype of NSObject, which means its designated initializer must override that of NSObject and it must call super.init() to initialize NSObject.

To retrieve the location updates provided by MapKit, add the following code to your LocManager class, replacing the comment // start updates above:

    func startUpdates() {
        if locManager.authorizationStatus == .notDetermined {
            // ask for user permission if undetermined
            // Be sure to add 'Privacy - Location When In Use Usage Description' to
            // Info.plist, otherwise location read will fail silently, with (lat/lon = 0)
            locManager.requestWhenInUseAuthorization()
        }
    
        Task {
            do {
                for try await update in CLLocationUpdate.liveUpdates() {
                    if let loc = update.location {
                        LocManagerViewModel.shared.setLocation(
                            lat: loc.coordinate.latitude,
                            lon: loc.coordinate.longitude,
                            speed: loc.speed)
                    }
                }
            } catch {
                print(error.localizedDescription)
            }
        }

        // start heading updates also
    }

    // update headings

To prompt the user for location permission, we first call CLLocationManager.requestWhenInUseAuthorization() to present the prompt. You can use either requestWhenInUseAuthorization() or requestAlwaysAuthorization(), corresponding to the permission key-value pair you listed in Info earlier.

After obtaining user’s permission, we use for try await to collect updates from the AsyncSequence returned by CloLocationUpdate.liveUpdates(). With each update, we call setLocation in LocManagerViewModel to update its observable location with the lat/lon and speed information in the latest update.

Unlike for location, MapKit does not provide an API to read heading updates as an AsyncSequence, instead it still relies on delegated callback to return heading updates. We now use the delegated callback mechanism to yield an event stream using AsyncStream. Add the following to your LocManager class, replacing // update headings comment above:

    var feeder: ((CLLocationDirection) -> Void)?

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        feeder?(newHeading.magneticHeading)
    } 

    var headings: AsyncStream<CLLocationDirection> { // getter
        AsyncStream(bufferingPolicy: .bufferingNewest(1)) { cont in
            feeder = { cont.yield($0) } // initialize feedr
            cont.onTermination = { @Sendable _ in
                Task { @MainActor in
                    self.locManager.stopUpdatingHeading()
                    self.feeder = nil
                }            
            }
            locManager.startUpdatingHeading()
        }
    }   

The property feeder() bridges the delegated callback mechanism to AsyncStream. We initialize it with a closure to yield the next stream element the callback mechanism delivers to the continuation that emits the next AsyncStream element.

We initialize feeder() in the property getter of headings to avoid Swift compiler complaining that we are initializing a property with another property when self is not ready.

We set AsyncStream to buffer only the latest element if updates arrive faster than we can read them (the default is “unbounded”). The elements emitted by AsyncStream comprises the AsyncSequence that we can loop through with for-await (in startUpdates in our case).

In the initialization closure, we also call CLLocationManager.startUpdatingHeading() to start receiving heading updates from the device driver. We stop the updates when the stream is terminated, using the continuation’s onTermination callback.

In this app, we have an endless supply of heading updates until we call stopUpdatingHeading(). If there is only a finite number of events, we can communicate end of stream to the continuation by calling cont.finish().

Add another Task to your startUpdates() method, right after the existing Task block, replacing the comment, // start heading updates also, to store the latest heading update to the headings property:

        Task {
            for await newHeading in headings {
                LocManagerViewModel.shared.setHeading(newHeading)
            }
        }

We will be needing location information in both MainView, to post a chatt, and when swiping left to MapsView, we therefore start collecting location updates upon app launch in swiftUIChatterApp, and do not stop for the lifetime of the app. Add the following code to your swiftUIChatterApp, inside the struct, before the body property:

    init() {
        LocManager.shared.startUpdates()
    }

Apple’s Map has its own mechanism to start and stop location and heading updates when the MapUserLocationButton is toggled and does not rely on our AsyncSequence.

Posting geodata

We can now obtain the user’s lat/lon and heading from iOS’s CLLocationManager. We can post this geodata information with each chatt to an updated Chatter backend that can handle the new APIs.

GeoData

Create a new GeoData struct to store the additional geodata in a new GeoData.swift file, to which we also import MapKit:

import MapKit

struct GeoData: Hashable {
    var lat: Double = 0.0
    var lon: Double = 0.0
    var facing: String = "unknown"
    var speed: String = "unknown"

    // do reverse geocoding
}

GeoData adopts the Hashable protocol because we use it in Chatt, which must be Hashable due to its use as a tag for map marker in MapView.

Reverse geocoding

In addition to lat/lon data, we use Apple’s MKReverseGeocodingRequest to perform reverse-geocoding to obtain human-friendly place names from the lat/lon data. Add the following place computed property to your GeoData struct, replacing the comment // do reverse geocoding:

    var place: String {
        get async {
            if let geolocs = try? await CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: lat, longitude: lon)) {
                    return geolocs[0].locality ?? geolocs[0].administrativeArea ?? geolocs[0].country ?? "place unknown"
                }            
            return "place unknown"
        }
    }

    // pretty print place

We will call setPlace() before posting a chatt to compute and include the place name in the posted chatt.

CLGeocoder deprecated in iOS 26

If you’re running iOS 26 and got the warning that CLGeocoder is deprecated, replace the code inside the get async {} block with:

            if let revGeocoder = MKReverseGeocodingRequest(
                location: CLLocation(latitude: lat, longitude: lon)),
               let geolocs = try? await revGeocoder.mapItems,
               let address = geolocs.first?.address {
                return address.shortAddress ?? (address.fullAddress.isEmpty ? "place unknown" : address.fullAddress)
            }

To present the geodata in a nicely formatted string, add the following computed property to your GeoData struct, replacing the comment, // pretty print place. We will use this property to display the geodata information to the user.

    var postedFrom: AttributedString  {
        get async {
            let place = await self.place
            var posted = try! AttributedString(markdown: "Posted from **\(place)** while facing **\(facing)** moving at **\(speed)** speed.")
            ["\(place)", "\(facing)", "\(speed)"].forEach {
                if !$0.isEmpty {
                    posted[posted.range(of: $0)!].foregroundColor = .blue
                }
            }
            return posted
        }
    }

Chatt

Add a new stored property geodata to the Chatt struct to hold the geodata associated with each chatt. We also make the struct conforms to the Hashable protocol to use it as a tag to identify selected map marker in MapView later.

struct Chatt: Identifiable, Hashable {
    var username: String?
    var message: String?
    var id: UUID?
    var timestamp: String?
    var geodata: GeoData?

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

ChattStore

We update postChatt(_:errMsg) to handle geodata. First, we serialize the geodata into a JSON Array, right below the postChatt(_:errMsg) function signature:

    func postChatt(_ chatt: Chatt, errMsg: Binding<String>) async {
        var geoObj: Data?
        if let geodata = chatt.geodata {
            geoObj = try? JSONSerialization.data(withJSONObject: [geodata.lat, geodata.lon, geodata.facing, geodata.speed])
        }
        
        // put geoObj into serialized chatt
    }

then we serialize the chatt to be posted, with the serialized geoObj inside it. Replace // put geoObj into serialized chatt with:

        let jsonObj = ["username": chatt.username,
                       "message": chatt.message,
                       "geodata": (geoObj == nil) ? nil : String(data: geoObj!, encoding: .utf8)]
        guard let requestBody = try? JSONSerialization.data(withJSONObject: jsonObj) else {
            errMsg.wrappedValue = "postChatt: JSONSerialization error"
            return
        }

        // update API endpoint

Finally, replace // update API endpoint with the update to postmaps URL:

        guard let apiUrl = URL(string: "\(serverUrl)/postmaps") else {
            errMsg.wrappedValue = "postChatt: Bad URL"
            return
        }

        // create and post request

The rest of the function to create and post the request remains the same. Keep the existing code.

Next we update getChatts(errMsg:). First update the API endpoint to the new getmaps URL. Replace getchatts to getmaps on this line:

        guard let apiUrl = URL(string: "\(serverUrl)/getmaps") else {

then replace the call to chatts.append() with:

                    let geoArr = chattEntry[4]?.data(using: .utf8).flatMap {
                        try? JSONSerialization.jsonObject(with: $0) as? [Any]
                    }
                    chatts.append(Chatt(username: chattEntry[0],
                                         message: chattEntry[1],
                                         id: UUID(uuidString: chattEntry[2] ?? ""),
                                         timestamp: chattEntry[3],
                                         geodata: geoArr.map {
                        GeoData(lat: $0[0] as! Double,
                                lon: $0[1] as! Double,
                                facing: $0[2] as! String,
                                speed: $0[3] as! String)
                    }))

The UI

Now we tackle the UI.

Posting geodata

To remind user that their location information will be sent along with their chatt, we display the user’s lat/lon alongside the text input box. In ContentView, wrap your TextField in a VStack so that the lat/lon is displayed below the TextField. At the same time, set the alignment parameter of the outher HStack() to .top, so that SubmitButton is aligned to the top of the TextField:

            HStack(alignment: .top) {
                VStack(alignment: .trailing) {
                    TextField(vm.instruction, text: Bindable(vm).message)
                        .focused($messageInFocus) // to dismiss keyboard
                        .textFieldStyle(.roundedBorder)
                        .cornerRadius(20)
                        .shadow(radius: 2)
                        .background(Color(.clear))
                        .border(Color(.clear))
                        .padding(.leading, 20)
                    Text("lat/lon: \(LocManagerViewModel.shared.location.lat)/\(LocManagerViewModel.shared.location.lon)")
                        .font(.caption)
                        .foregroundColor(Color(.systemGray3))
                        .padding(.trailing, 20)
                }
                SubmitButton(scrollProxy: $scrollProxy)
            }

Then in your SubmitButton, replace the call to postChatt(_:errMsg:) with the following:

                let geodata = GeoData(lat: LocManagerViewModel.shared.location.lat, lon: LocManagerViewModel.shared.location.lon, facing: LocManagerViewModel.shared.compassHeading, speed: LocManagerViewModel.shared.speed)

                await ChattStore.shared.postChatt(Chatt(username: vm.username, message: vm.message, geodata: geodata), errMsg: Bindable(vm).errMsg)

Displaying geodata

We need to add some map-related view states to our view model. Add import MapKit to the top of your swiftUIChatterApp.swift file:

import MapKit

Then add the following properties to ChattViewModel:

    var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)
    var selected: Chatt? = nil
    var showMap = false

We don’t display geodata information directly on the text bubble. Instead, if a chatt has geodata information, we display a map pin in its bubble, next to the message. Due to the alternating alignment display of text bubbles, depending on whether the user is the poster, we put the map pin icon on the right or left of the message, to go with the text bubble alignment. In ChattScrollView.swift, add import MapKit to the top of the file:

import MapKit

Then in the ChattView struct, add a property to grab the ChattViewModel from the SwiftUI environment:

    @Environment(ChattViewModel.self) private var vm

and wrap the Text displaying the message in an HStack so that a map pin icon is displayed either to its left (isSender) or right (!isSender). Note that we move the modifiers for the text bubble to encompass the whole HStack:

                HStack(alignment: .top, spacing: 10) {
                    if isSender, let geodata = chatt.geodata {
                        Image(systemName: "mappin.and.ellipse")
                            .foregroundStyle(.white)
                            .font(.caption)
                            .padding(.top, 3)
                            // tap to show geodata on map
                    }
                    
                    Text(msg)
                    
                    if !isSender, let geodata = chatt.geodata {
                        Image(systemName: "mappin.and.ellipse")
                            .foregroundStyle(.blue)
                            .font(.caption)
                            .padding(.top, 3)
                            // tap to show geodata on map
                    }
                }
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .background(Color(isSender ? .systemBlue : .systemBackground))
                .foregroundColor(isSender ? .white : .primary)
                .cornerRadius(20)
                .shadow(radius: 2)
                .frame(maxWidth: 300, alignment: isSender ? .trailing : .leading)

When user taps the map pin icon, we want to navigate to another screen showing a map with the poster’s location as pin on the map. To achive that, we add a tap gesture recognizer to both map pin icons. Replace the two instances of // tap to show geodata on map with:

                            .onTapGesture {
                                vm.selected = chatt
                                vm.cameraPosition = .camera(MapCamera(
                                        centerCoordinate: CLLocationCoordinate2D(latitude: geodata.lat, longitude: geodata.lon), distance: 500, heading: 0, pitch: 60))
                                vm.showMap.toggle()
                            }

When the user taps on a map pin icon, we say that the associated chatt is selected and store that in our view model, then we set the cameraPosition in the view model to that of the selected poster’s location, and we tell SwiftUI to show the map.

To actually navigate to the screen showing the map, in your ContentView, add the following as a modifier to your outermost VStack, e.g., before navigationTitle():

        .navigationDestination(isPresented: Bindable(vm).showMap) {
            MapView()
        }

MapView

We use MapView to show a poster’s location when the user clicked on the map pin as described. But we want MapView to also be able to show the locations of all the chatts retrieved from the backend.

Create a new Swift file MapView.swift and put the following imports and MapView struct in the file. We first implement putting a marker at the selected poster’s location:

import MapKit
import SwiftUI

struct MapView: View {
    @Environment(ChattViewModel.self) private var vm
    @State private var selection: Chatt?
    
    var body: some View {
        Map(position: Bindable(vm).cameraPosition, selection: $selection) {
            if let chatt = vm.selected {
                if let geodata = chatt.geodata {
                    Marker(chatt.username!, systemImage: "figure.wave",
                           coordinate: CLLocationCoordinate2D(latitude: geodata.lat, longitude: geodata.lon))
                    .tint(.red)
                    .tag(chatt)
                }
            // or positions of all chatts
            }

            // show chatt associated with clicked marker

            UserAnnotation() // shows current user's location as an ellipse at all time
        }
    }
}

If no particular chatt were selected, we show the position of all retrieved chatts. We loop through the chatts array in ChattStore and display a marker for each chatt, at the coordinates (lat/lon) the chatt was posted from. Replace // or positions of all chatts with:

            } else {
                ForEach(ChattStore.shared.chatts, id: \.self) { chatt in
                    if let geodata = chatt.geodata {
                        Marker(chatt.username!, systemImage: "figure.wave",
                               coordinate: CLLocationCoordinate2D(latitude: geodata.lat, longitude: geodata.lon))
                        .tint(.mint)
                    }
                }

If the user further taps on a marker shown, we display an info window showing the chatt associated with that marker. Replace // show chatt associated with clicked marker with:

            if let chatt = selection, let geodata = chatt.geodata {
                Annotation(chatt.username!, coordinate: CLLocationCoordinate2D(latitude: geodata.lat, longitude: geodata.lon), anchor: .topLeading
                ) {
                    InfoView(chatt: chatt)
                }
                .annotationTitles(.hidden)
            }

In rendering the Map, we center the map where the cameraPosition is pointing at (lat/lon), at the given zoom level (distance and height), and pitch (angle) of the camera.

Additionally, UserAnnotation() shows the current user’s current location.

The following modifiers of Map add:

  1. a button to center on the user’s location,
  2. a compass on the map, and
  3. a scale control
         .mapControls {
             MapUserLocationButton()
             MapCompass()
             MapScaleView()
         }
    

Here’s the definition of the InfoView to display a chatt on the map. Place it outside MapView:

struct InfoView: View {
    let chatt: Chatt
    @State private var postedFrom: AttributedString = ""
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                if let username = chatt.username, let timestamp = chatt.timestamp {
                    Text(username).padding(EdgeInsets(top: 4, leading: 8, bottom: 0, trailing: 0)).font(.system(size: 16))
                    Spacer()
                    Text(timestamp).padding(EdgeInsets(top: 4, leading: 8, bottom: 0, trailing: 4)).font(.system(size: 12))
                }
            }
            if let message = chatt.message {
                Text(message).padding(EdgeInsets(top: 1, leading: 8, bottom: 0, trailing: 4)).font(.system(size: 14)).lineLimit(2, reservesSpace: true)
            }
            if let geodata = chatt.geodata {
                Text(postedFrom)
                    .task { postedFrom = await geodata.postedFrom }
                    .padding(EdgeInsets(top: 0, leading: 8, bottom: 10, trailing: 4)).font(.system(size: 12)).lineLimit(2, reservesSpace: true)
            }
        }
        .background {
            Rectangle()
                .fill(.ultraThinMaterial)
                .cornerRadius(4.0)
        }
        .frame(width: 300)
    }
}

Displaying the geodata of all chatts

To display the geodata of all chatts, we add a map button to the title bar of the timeline screen. Add import MapKit to the top of your ContentView.swift:

import MapKit

Then add the following modifier to the outermost VStack of your ContentView, e.g., after navigationBarTitleDisplayMode():

        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Image(systemName: "map")
                    .foregroundStyle(.blue)
                    .onTapGesture {
                        vm.selected = nil
                        vm.cameraPosition = .camera(MapCamera(
                            centerCoordinate: CLLocationCoordinate2D(latitude: LocManagerViewModel.shared.location.lat, longitude: LocManagerViewModel.shared.location.lon), distance: 500, heading: 0, pitch: 60))
                        vm.showMap.toggle()
                    }
            }
        }

the action when user clicks on this button is similar the action for the other top gestures above, though here we specify that no chatt has been selected and we place the cameraPosition at the user’s current location.

To allow user to swipe left, in lieu of clicking the map button, to view all chatts, add the following modifier to ChattScrollView(), inside the ScrollViewReader() block of your ContentView, e.g., undere the .refreshable() modifier:

                    .gesture(DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
                        .onEnded { value in
                            if case (...0, -100...100) = (value.translation.width, value.translation.height) {
                                
                                vm.selected = nil
                                vm.cameraPosition = .camera(MapCamera(
                                    centerCoordinate: CLLocationCoordinate2D(latitude: LocManagerViewModel.shared.location.lat, longitude: LocManagerViewModel.shared.location.lon), distance: 500, heading: 0, pitch: 60))
                                vm.showMap.toggle()
                            }
                        }
                    )

Congratulations! You’re done with the front end! (Don’t forget to work on the backend!)

Run and test to verify and debug

Once you have your backend setup, run your front end against your back end. Due to location privacy concerns mada.eecs.umich.edu is not available for this tutorial. You will not get full credit if your front end is not set up to work with your backend!

Simulating locations

While running your project in Xcode by clicking the location arrow button in the debug console menu (screenshot) or access the feature from the main menu Debug > Simulate Location. When you post a chatt, or when you view all chatts, the user’s current location should be the simulated location. You can select a different location in Xcode and it should again be reflected in Chatter. Simulating location on Xcode effects only code run from, and still attached to, Xcode. The code can be running on device, attached to Xcode though a USB cable or WiFi, or it can be running on the simulator.

Front-end submission guidelines

We will only grade files committed to the main branch. If you use multiple branches, please merge them all to the main branch for submission.

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

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

  reactive
    |-- chatterd
    |-- chatterd.crt
    |-- maps
        |-- swiftUIChatter
            |-- swiftUIChatter.xcodeproj
            |-- swiftUIChatter
    # and other files or folders   

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

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

Review your information on the Tutorial and Project Links sheet. If you’ve changed your teaming arrangement from previous tutorial’s, please update your entry. If you’re using a different GitHub repo from previous tutorial’s, invite eecsreactive@umich.edu to your new GitHub repo and update your entry.

References


Prepared by Wendan Jiang, Alexander Wu, Benjamin Brengman, Ollie Elmgren, Nowrin Mohamed, Chenglin Li, Xin Jie ‘Joyce’ Liu, Yibo Pi, and Sugih Jamin | Last updated: August 13th, 2025