A simple iOS app that fetches Stack Overflow users and lets you follow/unfollow them locally.
The app shows a table of Stack Overflow users with their avatars, reputation, and a follow button. You can scroll through the list, follow or unfollow users, and if the network fails you get an error screen with a retry action.
On launch, the app fetches the first page of users from the Stack Overflow API.
As cells appear, it loads and caches their avatars. Follow status is persisted with Core Data, so it survives app restarts.
- Xcode 26.0
- iOS 26.0
- Swift 5
- Clone the repo and open
StackOverflowDemo.xcodeproj - Local modules are resolved via SPM, no external dependencies.
- Build and run on simulator or device
I split the app into separate Swift packages + Main Target:
- NetworkLayer
- PersistenceLayer
- NavigationLayer
- ImageLoader
This is deliberately more structure than a single screen app requires. The goal is to demonstrate clear module boundaries and isolated testability, so the architecture is ready to scale as the app grows.
I chose MVVM + Coordinators to show how this pattern can be built and maintained without external dependencies. View controllers hold no business logic, that's the responsibility of the ViewModel, observed through Swift's @Observable.
The coordinator pattern keeps navigation logic out of view controllers. Even with a single screen today, adding more flows later is straightforward.
All asynchronous work (networking, image loading, persistence) uses async/await. The result is a consistent, linear syntax across the codebase with no mixing of callback or Combine styles.
The table is backed by UITableViewDiffableDataSource, which handles animations automatically when data changes. Snapshots are produced by the ViewModel and applied in the view controller via updateProperties.
I used CoreData instead of UserDefaults because:
- Follow status is keyed by user ID, and makes querying and updating those records natural.
- It scales cleanly if more persisted entities are added later.
When the network fails, the view controller doesn't show a generic alert.
The ViewModel wraps the failure in an ErrorPlaceholderViewModel that describes what to render (title, message, retry action) without coupling the view to any specific error type. The same placeholder can be reused for empty states or other failure modes.
The ViewModel tracks currentPage and appends new users when you load more. Infinite scroll isn't wired up in this demo, but the plumbing is in place: triggering loadData() with an incremented page from a prefetch data source is all that's needed, and the diffable data source takes care of the animation.
Images load asynchronously when cells appear. If a cell is reused before the image loads, the old task gets cancelled so you don't see stale images in the wrong cells. This is handled in UserCellViewModel.loadImageIfNeeded() with a stored imageLoadTask that you cancel before starting a new one.
User names from the API come with HTML entities like ó instead of ó. I added a @HTMLDecoded property wrapper that decodes these using regex during JSON decoding, so the ViewController never sees raw entities. It's cleaner than post-processing.
I used the Swift Testing framework instead of XCTest because it's cleaner and more readable. Tests cover:
- Happy paths and error cases
- State transitions
- Retry logic
Localization is provided through a String Catalog (for the demo only English is provided)
- Pagination with infinite scrolling through PrefetchDataSource
- Screen Based modules, each app section could lay in its own module, to optimise build / test times
- Snapshot Testing using https://github.com/pointfreeco/swift-snapshot-testing
- UI Tests for essential flows
- Test navigation flow when more screen are added