Andrei Simvolokov ssimboss@gmail.com Technical Challenge up for a round-up bank feature
The App is written using XCode 26.1 and Swift 6.0 to work on iOS 26.1. Then downgraded to XCode 16.2 and iOS 18.2(see the latest commits). All the code is written using UIKit for UI-level. The code uses the patterns and API of the modern Swift Concurrency paradigm.
The App does not have an implementation of OAuth and does not automatically refresh its tokens. Instead, it completely relies on hardcoded Access.accessToken. Please configure this property first. Otherwise the App would block your user experience.
The App contains a few screens.
This is a start screen. It loads and displays user's accounts as a list. Supports zero case and failed-to-load state. Use pull-to-refresh to completely reload the list. Tap any account to proceed to "Account" screen
This is an account summary. It displays the balance, the list of Saving Goals and the list of Recent transactions(Transactions for the last 10 days). Supports zero cases and failed-to-load states. Use pull-to-refresh to completely reload the content. Use the "+" button in Saving Goal's header to proceed to the "Create Savings goal" screen. If you have at least one goal, tap on its cell to proceed to the "Saving Goal" screen.
This is an input-form to create a new goal. Provide a name for a goal and the target amount in the text-field and tap the "Create goal" button to initiate a process. Supports basic validation: button is disabled whenever goal's name is empty or the goal's target is zero. The "amount" text-field supports the basic formatting checks to prevent non-appropriate input.
This is a screen showing basic Savings Goal's details. Tap "Add round-ups!" button to proceed to the "Round up" screen. Tap the "Delete Goal" button to initiate goal removal and return to the "Account" screen.
This screen shows a list of transactions on current week(Since Monday for UK, depends on the localisation specifics). The amount of total possible round-ups is displayed in the header. Supports no-transaction case, zero-round-ups and failed-to-load cases. Tap the "Move round-ups to savings" button to initiate a transaction and return to the "Account" screen.
- Dark Mode support
- Localisation: English and Russian
- Accessibility: Voice Over A11y labels and A11y identifiers for 3rd party QA tools
- Liquid glass icon
The App project contains next domains:
These are all the resources consumed by the App: localisations, assets(colors and images), AppIcon.
This is all the code related to the View-Level of the App. All the View-Controllers follow the MVVM architecture: they display the values provided by their view-models, observe any changes in the state and provide information about the user's actions back to their view-models.
Design.swift. These are global variables containing basic constants of a primitive design-system. Currently it has only a set of colors and fonts.AllAccounts. This is the code responsible for displaying the "All accounts" screen. This folder contains aAllAccountsViewControllerand the cells it uses.Account. This is the code responsible for displaying the "Account" screen. This folder contains aAccountViewController, the cells and subviews it uses.CreateSavingsGoal. Contains a singleCreateSavingsGoalViewControllerresponsible for the "Create Savings goal" screen.SavingGoal. Contains a singleSavingsGoalViewControllerresponsible for the "Savings goal" screen.RoundUp. Contains a singleRoundUpViewControllerresponsible for the "Round up" screen.
These are all the ViewModels and additional helpers which work on the MainActor/DispatchQueue.main. The structure mirrors the Views. The helpers are:
Factory: These are simple classes to construct a specified ViewController.Router: These are simple classes responsible for redirections from particular screens
The View-Model behaviour described below:
AllAccountsViewModel: Observes the list of current accounts and their balances inAccountsService. UsesDataFormatterto display currency-values properly. Forces the update on construction. Forces a complete data-reload on pull-to-refresh. Redirects to accounts screen through its router on account-cell tap. Shows alert via router on appear if theAccess.accessTokenis not configured.AccountViewModel: Observes the balance of the displayed account inAccountsService, the list of recent transaction inTransactionsServiceand the list of saving-goals inSavingsGoalsService. UsesDataFormatterto display currency-values and dates properly. Sorts the data-lists before being displayed. Forces a complete data-reload on pull-to-refresh. Redirects to other screens on user actions when needed.CreateSavingsGoalViewModel: Handles the user's input actions and does a primitive data-validation. Enables the CTA state when data is valid. UsesDataFormatterto display currency-values properly. Initiates creation of a Savings Goal withSavingsGoalsService. Dismiss itself using its router.SavingGoalViewModel: Displays data of a selectedSaving Goal. UsesDataFormatterto display currency-values properly. Initiates a Savings Goal's deletion on button tap withSavingsGoalsService. Redirects to the "Round Up" screen with the router when needed. Dismiss itself using its router.RoundUpViewModel: Observes the list of weekly transactions in another instance ofTransactionsService. Forces the list reload on construction. Sorts data before displaying. UsesDataFormatterto display currency-values properly. UsesSavingsGoalsServiceto initiate a money-transaction.
This is all the code which is responsible for data and data transformations in the App.
DataFormatter.swift: A simple formatter class used by View-Models to properly display currency and date values. Also used to validate the currency-inputs.Foundation: Contains protocols to be used instead of actualFoundationframework instances(FileManager,URLSession). Contains aLoadabletype to handle loadable values.Entities: These are all the entity-structures used by the App.Network: Contains aNetworkService. This is a basic class to execute HTTP REST requests.API: Contains aBankAPI. It uses theNetworkServiceto execute a request of specific domains(AccountsAPI,SavingsGoalAPI,TransactionsAPI).Cache: Contains a genericCacheServiceto store any kind ofDecodabledata in a special file on a device.DataServices: Contains services responsible for particular data sets of Model-level. See the details below.
Provides a list of current Accounts. Also provides an AsyncStream to subscribe for any changes in this list. Implements methods to do a hard reloading of the list and soft-update(silence reload without falling in a loading state).
Also provides the balances for the accounts and subscription streams for the balances. Implements methods to do a hard or soft balance reload.
Uses AccountsAPI for remote operations. Stores accounts and balances in two separate instances of CacheService.
Provides a list of current Saving Goals. Also provides an AsyncStream to subscribe for any changes in this list. Implements methods to do a hard or soft reloading of the list, to create or delete Savings Goals, to move money into Savings Goals.
Uses SavingsGoalAPI for remote operations. Stores the list in an instance of CacheService. Triggers updates of balances, recent transactions and weekly transactions on some operations(Moving money to the Savings Goal, deletion of the Savings Goal).
Provides a list of transactions. Also provides an AsyncStream to subscribe for any changes in this list. Implements methods to do a hard or soft reloading of the list.
Uses TransactionsAPI for remote operations. Stores the list in an instance of CacheService.
The app has two instances of this class:
recentTransactionsService: handles transactions done in the last 10 days.weekTransactionsService: handles transactions done since the beginning of the week(Monday for UK, depends on localisation).
All the classes in the project are written in protocol-based manner to make possible 100% test coverage. However, the project itself misses the init tests for all the components due to lack of time for implementation. Currently there are only some tests to show the basic principles of unit-tests.
The UnitTests host the App itself due to the problems with default project configuration in XCode 26.1.