Skip to content

Spawn: Spontaneity made easy. This app aims to ease the friction of making spontaneous plans with your friends, and get rid of the infinite group chat headache.

License

Notifications You must be signed in to change notification settings

Daggerpov/Spawn-App-iOS-SwiftUI

Repository files navigation

Spawn-App-iOS-SwiftUI

Table of contents:

Onboarding

Links

Setup

Running through an iPhone Simulator (for testing end-to-end functionality)

This is simply done through clicking here (or Cmd+R):

alt text

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: alt text 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"

alt text

However, you'll only be able to preview SwiftUI files that include this section here (typically at the bottom of the file):

alt text

More complicated case, to supply initial state and init() parameters:

alt text


Code Explanations

API Calls

API Calls

  • In our codebase, we do these API calls from within ViewModels, which leverage the IAPIService interface methods, implemented in APIService.swift and implemented as mocks in MockAPIService.swift

    • An example of this is here: alt text
    • 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
      • The URL is concatenated with our back-end's base URL defined here: alt text
    • We surround all code with a do-catch, similar to other languages' try-catch blocks.
  • APIService.swift

    • This makes actual GET, POST, PUT, and DELETE requests to our back-end API
      • Additionally, there's a special createUser() method, since that request also takes in raw image data for the user's profile picture alt text
    • Decodable and Encodable are used to serialize and deserialize JSON data, so we need our Models/ classes to conform (implement) these protocols
      • Codable makes them conform to both: alt text
    • The parameters argument is used for argument parameters, like in a URL as you'd see /events?requestingUserId=1 for example -> we then construct a finalURL alt text
    • The URLSession.shared.dataTask method is used to make the actual request alt text
    • Then, we handleAuthTokens() to deal with JWTs (JSON Web Tokens) sent from the back-end, which comprises the access token and refresh token
      • This stores those tokens in the Keychain, on the user's device, for future requests alt text
    • 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

    • This is where we define the mocked data that we use for testing purposes, when the isMocked variable is set to true in MockAPIService.swift
    • We do this by matching up the requested URL with its requesting data type in a return as such: alt text
Asynchrony in SwiftUI

Asynchrony in SwiftUI

  • onAppear{} is a way to run a function when a view appears
    • This is similar to React's useEffect() hook alt text
  • 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.
  • 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 alt text
SwiftUI Syntax

SwiftUI Syntax

  • @State is a mutable variable, and works similarly to React state variables, except without setState() 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 a ViewModel object alt text alt text
    • @Published is a way to publish changes in a variable, and is used for publishing changes in a ViewModel object
      • This will work similarly to @State, except that it will be used in a ViewModel object, and published to a parent view through @ObservedObject alt text
  • 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, and ZStack are ways to group components (like divs in HTML) across dimensions: horizontally, vertically, and in the Z-dimension alt text
  • Button{} is a way to create a button in SwiftUI
    • the action parameter is where you define what happens when the button is clicked
    • the other closure is the label closure, as such, which display the button's UI alt text
  • NavigationLink{} is a way to navigate to another view in SwiftUI, as a button alt text
  • 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
}
MVVM Architecture

MVVM Architecture

alt text

Mobile Caching Implementation

Mobile Caching Implementation

The app implements client-side caching to improve performance and reduce API calls. The caching system includes:

  1. AppCache Singleton: A centralized cache store that persists data to disk and provides reactive updates
  2. Cache Validation API: A backend API endpoint that validates cached data and informs the client when to refresh
  3. Push Notification Support: Real-time updates when data changes server-side

Overview

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:

  1. Client-side caching: Storing frequently accessed data locally
  2. Cache invalidation: Checking with the backend to determine if cached data is stale
  3. Push notifications: Receiving real-time updates when relevant data changes

Components

AppCache Singleton

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)
        }
    }
}

Cache Validation API

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
  }
}

Push Notification Handling

The app listens for push notifications with specific types that indicate data changes:

  • friend-accepted: When a friend request is accepted
  • event-updated: When an event is updated

When these notifications are received, the app refreshes the relevant cached data.

How It Works

  1. On app launch, AppCache loads cached data from disk
  2. The app sends a request to validate the cache with the backend
  3. 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
  4. As the user uses the app, they see data from the cache immediately
  5. In the background, the app may update cache items based on push notifications

Implementation Details

Data Flow

  1. App loads cached data → UI renders immediately
  2. App checks if cache is valid → Updates UI if needed
  3. User interacts with fresh data → Great experience!

Benefits

  • Speed: UI renders instantly from cache
  • Bandwidth: Reduced API calls
  • Battery: Less network activity
  • Offline Use: Basic functionality without network

Testing the Cache

To verify the cache is working:

  1. Launch the app and navigate to a screen that displays cached data (e.g., friends list)
  2. Put the device in airplane mode
  3. Close and reopen the app
  4. The data should still be displayed, loaded from the cache

Cache Limitations

The current implementation has some limitations:

  1. Cache is stored in UserDefaults, which has size limitations
  2. No encryption for cached data
  3. No automatic pruning of old cached data
  4. Limited offline editing capabilities

These could be addressed in future updates.

For complete implementation details, see the cache-implementation-guide.md file.


Current App Look

Note that this is usually behind our actual 'current' app; stay tuned on getspawn.com for our beta for the actual app

Legacy Screenshots:

Entity Relationship Diagram

erd-nov-21

About

Spawn: Spontaneity made easy. This app aims to ease the friction of making spontaneous plans with your friends, and get rid of the infinite group chat headache.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages