- 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 through an iPhone Simulator (for testing end-to-end functionality)
This is simply done through clicking here (or Cmd+R):
Previewing within XCode (for quickly seeing UI changes)
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
ViewModel
s, which leverage theIAPIService
interface methods, implemented inAPIService.swift
and 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-catch
blocks.
- An example of this is here:
-
APIService.swift
- This makes actual GET, POST, PUT, and DELETE requests to our back-end API
Decodable
andEncodable
are used to serialize and deserialize JSON data, so we need ourModels/
classes to conform (implement) these protocols- The
parameters
argument is used for argument parameters, like in a URL as you'd see/events?requestingUserId=1
for example -> we then construct afinalURL
- The
URLSession.shared.dataTask
method 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
User
object for a GET request to/users/1
- If there's an error in any capacity, we throw it so that the calling
ViewModel
class 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
@State
is a mutable variable, and works similarly to React state variables, except withoutsetState()
methods@Binding
is a way to pass a@State
variable from a parent view to a child view
@ObservedObject
is a way to observe changes in an object, and is used for observing changes in aViewModel
objectvar 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
, andZStack
are ways to group components (like divs in HTML) across dimensions: horizontally, vertically, and in the Z-dimensionButton{}
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
@Published
properties 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 {
@EnvironmentObject var appCache: AppCache
var body: some View {
List(appCache.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,
AppCache
loads 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.
Note that this is usually behind our actual 'current' app; stay tuned on getspawn.com for our beta for the actual app
