- Back-End Repository
- Swift Learning Resources
- SwiftUI Learning Resources
- Ask Daniel for:
- Figma link
- TestFlight invite, to try the app on your own phone
Running the app (iOS Simulator & XCode Canvas)
Firstly, in MockAPIService.swift is where you'll be able to dictate whether the app is being mocked, through the isMocked variable, as you can see here:
This should be set to true if you're working on a UI-specific feature that will be previewed within XCode often for making UI tweaks.
On that topic, to preview within XCode, you can toggle that through toggling "Edit" -> "Canvas"
However, you'll only be able to preview SwiftUI files that include this section here (typically at the bottom of the file):
More complicated case, to supply initial state and init() parameters:
API Calls
-
In our codebase, we do these API calls from within
ViewModels, which leverage theIAPIServiceinterface methods, implemented inAPIService.swiftand implemented as mocks inMockAPIService.swift- An example of this is here:

- As you can see, the method is marked
async - It interfaces with our back-end API, as the URLs match up with our endpoints in the back-end
Controllers/directory - We surround all code with a
do-catch, similar to other languages'try-catchblocks.
- An example of this is here:
-
APIService.swift- This makes actual GET, POST, PUT, and DELETE requests to our back-end API
DecodableandEncodableare used to serialize and deserialize JSON data, so we need ourModels/classes to conform (implement) these protocols- The
parametersargument is used for argument parameters, like in a URL as you'd see/events?requestingUserId=1for example -> we then construct afinalURL
- The
URLSession.shared.dataTaskmethod is used to make the actual request
- Then, we
handleAuthTokens()to deal with JWTs (JSON Web Tokens) sent from the back-end, which comprises the access token and refresh token - Afterward, we ensure:
- The status code is what we expect, like 204 for a successful DELETE request or 200 for a successful GET request
- The data can be decoded into what we expect, like a
Userobject for a GET request to/users/1 - If there's an error in any capacity, we throw it so that the calling
ViewModelclass can catch it and deal with it through UI or state updates accordingly
-
MockAPIService.swift
Asynchrony in SwiftUI
onAppear{}is a way to run a function when a view appears- The
Task{}closure, is a way to run an asynchronous functions in SwiftUI- This ensures that this piece of code is ran asynchronously, and that the UI is not blocked on the main thread, since anything in
Task{}runs on the background thread.
- This ensures that this piece of code is ran asynchronously, and that the UI is not blocked on the main thread, since anything in
MainActor.run{}is a way to run a function on the main thread, and is used to update the UI- This is similar to React's
setState()method - This is essentially the inverse of
Task{}in that it runs on the main thread, and is used to update the UI, from within a background thread - One application would be when you're fetching data from within a ViewModel class (which is on a background thread), and you want to update the UI with that data, you would use
MainActor.run{}to update the UI with that data
- This is similar to React's
SwiftUI Syntax
@Stateis a mutable variable, and works similarly to React state variables, except withoutsetState()methods@Bindingis a way to pass a@Statevariable from a parent view to a child view
@ObservedObjectis a way to observe changes in an object, and is used for observing changes in aViewModelobject
var body: some View{}is the main body of a SwiftUI view, and is where the UI is defined- This is similar to React's return statement in a functional component
HStack,VStack, andZStackare ways to group components (like divs in HTML) across dimensions: horizontally, vertically, and in the Z-dimension
Button{}is a way to create a button in SwiftUINavigationLink{}is a way to navigate to another view in SwiftUI, as a button
- Unwrapping Optionals (4 ways):
In this example, app.users is an optional value, a.k.a. it might be nil (null in other languages). Thus, we need to 'unwrap' it to get the actual value, and here are the 4 ways to do this in Swift:
let appUsers = appPortfolio.map {app in
// Force-Unwrap
return app.users! // leads to error if app.users is nil -> very dangerous
// guard unwrap, for early-exits
guard let userCount = app.users else {
return 0
}
return userCount
// if unwrap, for quick usage, but without persistence of guard
if let userCount = app.users {
return userCount
} else {
return 0
}
// nil coalescing to provide default value
// example of good default would be "Not Given" for a user's username
return app.users ?? 0
}
Mobile Caching Implementation
The app implements client-side caching to improve performance and reduce API calls. The caching system includes:
- AppCache Singleton: A centralized cache store that persists data to disk and provides reactive updates
- Cache Validation API: A backend API endpoint that validates cached data and informs the client when to refresh
- Push Notification Support: Real-time updates when data changes server-side
The Spawn App iOS client implements a sophisticated caching mechanism to reduce API calls, speed up the app's responsiveness, and provide a better user experience. This is achieved through:
- Client-side caching: Storing frequently accessed data locally
- Cache invalidation: Checking with the backend to determine if cached data is stale
- Push notifications: Receiving real-time updates when relevant data changes
The AppCache class is a singleton that manages the client-side cache:
- Stores cached data in memory using
@Publishedproperties for reactive SwiftUI updates - Persists cached data to disk using
UserDefaults - Validates cache with backend on app launch
- Provides methods to refresh different data collections
Example of using the AppCache:
// Access cached friends in a view
struct FriendsListView: View {
// Access AppCache directly through the shared singleton
var body: some View {
List(AppCache.shared.friends) { friend in
FriendRow(friend: friend)
}
}
}The app makes a request to /api/v1/cache/validate/:userId on startup, sending a list of cached items and their timestamps:
{
"friends": "2025-04-01T10:00:00Z",
"events": "2025-04-01T10:10:00Z"
}The backend responds with which items need to be refreshed:
{
"friends": {
"invalidate": true,
"updatedItems": [...] // Optional
},
"events": {
"invalidate": true
}
}The app listens for push notifications with specific types that indicate data changes:
friend-accepted: When a friend request is acceptedevent-updated: When an event is updated
When these notifications are received, the app refreshes the relevant cached data.
- On app launch,
AppCacheloads cached data from disk - The app sends a request to validate the cache with the backend
- For invalidated cache items:
- If the backend provides updated data, it's used directly
- Otherwise, the app fetches the data with a separate API call
- As the user uses the app, they see data from the cache immediately
- In the background, the app may update cache items based on push notifications
- App loads cached data → UI renders immediately
- App checks if cache is valid → Updates UI if needed
- User interacts with fresh data → Great experience!
- Speed: UI renders instantly from cache
- Bandwidth: Reduced API calls
- Battery: Less network activity
- Offline Use: Basic functionality without network
To verify the cache is working:
- Launch the app and navigate to a screen that displays cached data (e.g., friends list)
- Put the device in airplane mode
- Close and reopen the app
- The data should still be displayed, loaded from the cache
The current implementation has some limitations:
- Cache is stored in
UserDefaults, which has size limitations - No encryption for cached data
- No automatic pruning of old cached data
- Limited offline editing capabilities
These could be addressed in future updates.
For complete implementation details, see the cache-implementation-guide.md file.
Map Integration
The app uses standard Apple MapKit for maps with a clean, basic styling:
- Native Apple mapping solution
- Clean, familiar interface for iOS users
- No API keys or external dependencies required
- Built-in user location tracking
- Efficient map annotations
- Consistent appearance with iOS system apps
- For event viewing:
MapView.swiftdisplays events as standard pins - For location selection:
LocationSelectionView.swiftimplements a draggable map with centered pin - Both implementations use a fixed pin approach where the map moves under the pin
The map implementation is designed to be simple and intuitive, with a clean aesthetic that matches iOS system apps.
Configuring Deeplink Share URLs
The app uses share URLs to allow users to share activities with others. By default, these URLs point to a GitHub Pages URL, but you can customize them:
- Open
Spawn-App-iOS-SwiftUI/Services/Constants.swift - Update the
shareBaseURL in theURLsstruct:
struct URLs {
// Base URL for sharing activities
// Option 1: Use GitHub Pages (recommended for web app)
static let shareBase = "https://daggerpov.github.io/spawn-app"
// Option 2: Use GitHub repository directly
// static let shareBase = "https://github.com/daggerpov/spawn-app"
// Option 3: Use a custom domain
// static let shareBase = "https://your-domain.com"
}The app will automatically generate share URLs in the format: {shareBase}/activity/{activityId}












