It has become a standard practice for iOS Development recruiters to shortlist candidates based on a take home coding assignment – And this coding walkthrough will help you with just that.
The thought process behind a coding assessment round are, majorly:
- Ensure only serious candidates apply - A coding assessment takes anywhwere between 2 to 7 hours to complete, only those who really want the opportunity will apply
- Interviewer can spend 15 minutes to get a good idea of how capable the coder is
It is equally important to understand what the shortlisting interviewer really seeks when they give out this assignment, since this is the coding test that can make or break your chances to interview with the company.
Typically, interviewers are looking for
– UI thinking and design capabilities
– Test cases and coverage
– Architecture and design patterns used to build the app
– Readability of code (documentation via comments)
Really good tech startups like Gojek clearly specify what they are looking for. viz. Architecture, Documentation, so that’s not really something you should be pondering about. However if the interviewer specifies there will be brownie points for adding test cases, never forget to do that!
The iOS MVVM Weather app
If you are new to iOS Development, a glance at how this app is built will help you incorporate what good interviewers expect, when they specify they want you to complete a coding assignment. We are going to build this app, with
– UIKit with MVVM architecture
– XIB based reusable views
– Coordinators for navigation
– Moya for networking
– RxSwift for reactive implementation
We will also be sprinkling some Singletons, Delegates, protocols and a couple of test cases. All of these are pretty basic, but when combined together can be used to build pretty scalable applications.
Swift with UIKit is the De facto iOS Dev stack most companies use, so we will be starting a new XCode project
But before that.. start by initializing a git repository. Why? Quality companies (again, taking the reference from Gojek) like it when a github repo is created before their candidates start the assessment, because in the real world, repositories are created before code is written.
Also, commit often – just like you would, in the real world.
I initialized the git repo with a readme (more on this, later) along with a .gitignore for swift and MIT license.
Now, clone it onto the desired location, and initialize the XCode project to get started with the real code.
After creating the XCode project in the git folder, I removed the unnecessary SceneDelegate and added these entries to Info.plist
And modified AppDelegate to show an empty Navigation Controller on launch
After the addition of this boilerplate code, run the app to ensure that we are good to move to the next phase of our development!
The first commit!
We have not even started with writing code for the actual app, however it’s always good to set it up. After all, well begun is half done.
Based on your app, you might want to add app icon, launch screen content before you make your first commit.
I went ahead to add a small weather icon on a plain white background onto AppIcon.co to get app icon assets in different sizes, to add to my App.
After customizing the launchscreen, I was still missing something. A proper folder structure!
Since the project navigator can get quite cluttered with lots of files, I went to create a bunch of folders and placing files under those folders
This file structure is purely a personal preference, however keeping things organized helps a lot to make a good start. After running the app and ensuring it still works as expected, I finally moved all changes to a new branch, committed and created a PR to merge it to develop branch. (Hint: This is how real projects are managed, probably a good idea to just do this in assignments too?)
Additionally, this also gives me the advantage of sharing links to download the source code, at every milestone –
https://github.com/iAkashlal/Weather-app/tree/feature/weather-app-wireframe
(If you want to take it from here, and implement side by side)
Adding networking to the weather app
APIs are the backbone of any application, and in this one we are going to try out OpenWeatherAPI’s weather by City API –
https://openweathermap.org/api/geocoding-api#direct.
Get your API keys ready by signing up for an account at OpenWeatherMap weather api provider.
We are going to use Moya for this purpose, so begin with adding Moya with SPM onto our project, by choosing ‘master’ branch
Test if the dependency import succeeded by importing Moya in the WeatherAPI.swift file which we have already created, and add in base URL for API and api Key in OpenWeather struct as static variables
Implementation of Moya is based on an enum, where each case is an API call that needs to be made. In our case, ‘searchText’ takes in a text parameter which user will type in, to get list of cities for which weather is to be shown.
On conforming the enum to TargetType and adding protocol stubs gives you a bunch of variables, namely
– baseURL: The base URL of all API calls that are going to be made
– path: The path to the particular API call
– method: GET, POST, UPDATE etc, switching on all enum cases to provide the proper value
– task: The HTTP task, namely the parameters of get request or json body of post request.
– headers: The headers to be sent in API requests
Based on our API call,
https://api.openweathermap.org/geo/1.0/direct?q=London&limit=15&appid=3a5fabe12f5bccdfca69c983db89f6bb
where
https://api.openweathermap.org/ – Base URL
geo/1.0/direct – Path for search city API
q = <searchText>, limit = 15, appid = <apiKey> are parameters of search city API get request
To make API requests, we need a MoyaProvider which is declared as a static variable, which can be now used to make API calls from within the viewmodel. Code can be accessed at
https://github.com/iAkashlal/Weather-app/tree/feature/moya-weather-fetcher
Implementing search list - MVVM Style
Now is the time to implement the first search list screen of the app. I prefer a ‘module’ based file structure, where different modules of the app are different directories. Eg. Auth Module can contain Login page, Signup page, Forgot password page.
We have already created a basic module directory with Weather Search as a single module containing Search List Controller
In MVVM Architecture, every page will have three components,
– Model
– View
– ViewModel
Model will be the structure of the Data returned from API, as well as any other models used in the ViewController.
View would be the ViewController containing only the design elements.
ViewModel will be a swift class with all business logic in it.
The view will be composed of two elements, one the UIViewController subclass and another the XIB which is the design representation. We can add that by New File > CocoaTouchClass > Name the class, checking the “Also create XIB file”
In the XIB created, add a search box, and try to get it show up as the first screen by setting this as navigationController’s first view controller. To load ViewControllers from XIB, adding a helper loadFromNib() function as an extension to UIViewController,
and then loading this viewcontroller as the main NavigationController’s base viewcontroller, we are able to get the search ViewController appear as soon as the app opens
Adding our first ViewModel
If you think about the functionalities of this search viewcontroller, it’s responsible for:
1. Get user input
2. Search for cities based on input with API
3. Display list of results from API
4. Display an empty state screen if API returns no result
5. Navigate to a detail screen when one of the list members are tapped on
1, 3, 4 and 5 are something that view controller needs to handle, while 2 is something that can be handled by the ViewModel.
Every viewcontroller has an associated viewmodel, with the viewController holding a strong reference to the viewModel.
The SearchVM would have one function, which takes in a String and makes the API call.
After API returns, it would fill in a variable, the list of cities. Whenever this variable changes, the viewcontroller should be binded to reload the table with new data obtained. If new data obtained is blank, then the empty screen should show up.
Translating this into code, and verifying by typing in something in search bar. Everything typed will be printed on console, due to the print functionality in viewModel.
Since it’s viewmodel’s job to fetch data, lets look at api request and response structure.
For an API request with search text “London”, https://api.openweathermap.org/geo/1.0/direct?q=London&limit=15&appid=3a5fabe12f5bccdfca69c983db89f6bb
responds with a JSON containing an array of objects with name (string), lat and lon of type Double and country + state of type Strings.
This will be added as a Model and will be decoded by ViewModel.
Another small change was required in WeatherAPI, since we are making a GET request, we need URLEncoding and not JSONEncoding in line 58 of previous code
And with this, we are able to fetch and populate the viewModel’s variable. Two things left now,
1. Add views in viewController to display list of cities
2. Reload views on viewmodel list change
Our viewcontroller can contain a simple tableview, with rows for displaying city names. When tapped, a detail view will be further shown. Under the module Weather Search, I prefer creating a folder ‘Components’ and having individual components in separate folders in this folder.
Creating a UITableViewCell subclass with New File > Cocoa Touch Class > SearchListTVC as UITableViewCell subclass with “Also create XIB file” checked under folder “Search TableViewCell”
Now would be a good time to add a folder to Assets.xcassets containing colors that our app will use. Under Assets, right click > New Folder named “Colors”.
Then, right click > new color set named “darkest.text” with hex #111111, “darker.text” with #222222, “dark.text” with #333333.
Also adding a Color enum to return these named colors as non optional UIColors, to ensure easy and consistent usage throughout the application
Moving to the SearchTVC, added two labels to display contents from API and set up the design in associated XIB and swift files,
Now, let’s
1. Adding a tableview to searchVC
2. Setup so that tableview shows data from viewModel’s list
3. Tableview reloads whenever viewmodel’s list changes
Adding a couple of Tableview helper extensions to register cells with ease,
1. ReusableView under Utility
import UIKit
/// Adopt this protocol on all subclasses of UITableViewCell and UICollectionViewCell
/// that use their own .xib file
public protocol NibLoadableView {
/// By default, it returns the subclass name
static var nibName: String { get }
/// Instantiates UINib using `nibName` as the name, from the main bundle
static var nib: UINib { get }
}
public extension NibLoadableView where Self: UIView {
static var nibName: String {
String(describing: self)
}
static var nib: UINib {
UINib(nibName: nibName, bundle: nil)
}
}
public protocol NibReusableView: ReusableView, NibLoadableView {}
/// Protocol to allow any UIView to become reusable view
public protocol ReusableView {
/// By default, it returns the subclass name
static var reuseIdentifier: String { get }
}
public extension ReusableView where Self: UIView {
static var reuseIdentifier: String {
String(describing: self)
}
}
extension UITableViewHeaderFooterView: NibReusableView {}
extension UITableViewCell: NibReusableView {}
extension UICollectionReusableView: NibReusableView {}
2. Extending UITableView in UITableView+ under Utlity > Extensions
//
// UITableView+.swift
// Weather
//
// Created by akashlal on 13/03/22.
//
import UIKit
public extension UITableView {
// register for the Class-based cell
func register(class _: T.Type) {
register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
}
// register for the Nib-based cell
func register(nib _: T.Type) {
register(T.nib, forCellReuseIdentifier: T.reuseIdentifier)
}
func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
fatalError("Dequeing a cell with identifier: \(T.reuseIdentifier) failed.")
}
return cell
}
// register for the Class-based header/footer view
func register(class _: T.Type) {
register(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
}
// register for the Nib-based header/footer view
func register(nib _: T.Type) {
register(T.nib, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
}
func dequeueReusableView() -> T? {
let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T
return view
}
func dequeueReusableView(type _: T.Type) -> T? {
let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T
return view
}
}
And adding tableview to searchVC XIB, outletting and connecting and adding more code to get the tableview to display the list,
class SearchVC: UIViewController {
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var searchTableView: UITableView! // 1
var viewModel = SearchVM()
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
self.title = "Search"
self.navigationController?.navigationBar.prefersLargeTitles = true
setupTableView()
}
private func setupTableView() {
searchTableView.register(nib: SearchListTVC.self)
searchTableView.delegate = self
searchTableView.dataSource = self
viewModel.reloadUI = { [weak self] in
self?.searchTableView.reloadData()
}
}
}
extension SearchVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
viewModel.performSearch(text: searchText)
}
}
extension SearchVC: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let location = viewModel.searchResultList?[indexPath.row] {
print("location with lat\(location.lat) and long \(location.lon) tapped" )
}
}
}
extension SearchVC: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.searchResultList?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SearchListTVC = searchTableView.dequeueReusableCell(forIndexPath: indexPath)
if let location = viewModel.searchResultList?[indexPath.row] {
cell.configure(for: location)
}
return cell
}
}
Adding tableviewcells to tableviews is always messy, but after adding our tableview helpers, you can see how easy it has become to add and how neat it looks, no strings attached (literally!).
Adding strings to code is almost always a bad thing, since a misspelling can break your app. This approach helped us to make the code more readable, as well as less error prone.
Our app now is capable of searching for cities, and displaying list of cities. All these changes will be moved to a branch and merged with develop, with a PR. Link to the final code of this milestone – https://github.com/iAkashlal/Weather-app/tree/feature/search-list
Also added in small aesthetic updates to code in commits – https://github.com/iAkashlal/Weather-app/commits/feature/search-list for the app to look like
Now, it's time for some enhancements!
Though the application is functional, we have missed a few things to make the app look production ready:
– Loader, when network call is loading
– No results page, when user query has no results
– Empty state page, when user hasn’t entered a search query
I prefer skeleton loaders whenever possible, to display loading lists. After adding the https://github.com/Juanpe/SkeletonView dependency via SPM, I added a protocol ‘SkeletonLoading’ under Utility > Protocols that makes adding SkeletonLoaders anywhere a breeze.
import UIKit
import SkeletonView
protocol SkeletonLoading: UIView {
/// Outlet collection for all preview views
var skeletableViews: [UIView] { get }
/// Any changes to be done after skeleton loader is disabled. Eg. Applying gradient layers to buttons
func updateSkeletonDesign(isEnding: Bool)
}
extension SkeletonLoading {
/// Call to begin skeleton animation
func startSkeletonLoading() {
DispatchQueue.main.async {
self.updateSkeletonDesign(isEnding: false)
self.skeletableViews.forEach { view in
view.isSkeletonable = true
view.showAnimatedGradientSkeleton()
if let textView = view as? UILabel {
textView.linesCornerRadius = 8
}
}
}
}
/// Call after data to be shown is available
func stopSkeletonLoading() {
DispatchQueue.main.async {
self.skeletableViews.forEach { view in
view.hideSkeleton()
}
self.updateSkeletonDesign(isEnding: true)
}
}
}
The protocol can be implemented by any UIView and on adding protocol stubs, it needs an array of views on which animations will be drawn, along with a function that notifies once skeleton animation is done playing.
startSkeletonLoading should be called whenever user text changes (and network hasn’t responded), while stopSkeletonLoading will be called after network responds.
Extending SearchListTVC and adding views that are animated,
extension SearchListTVC: SkeletonLoading {
var skeletableViews: [UIView] {
return [
cityTitle,
citySubtitle
]
}
func updateSkeletonDesign(isEnding: Bool) {
return
}
}
And then adding a configure function for when cell is to be shown in loading state,
Since networking is done by the viewModel, adding a isLoading bool property to the viewModel which changes whenever network loading starts/stops, which will be passed on to the tableviewcell to display loading animations
Now with the loader added, let's add up empty state pages for when search bar is empty, and when search bar isn't empty but API responds with no results
The empty state view can be our tableview’s background view, hence adding a condition to show the background view when there are no results, and hide it when results are shown work work great – https://github.com/iAkashlal/Weather-app/commit/ca2a154dd36b4e3c4a515cde27013ec7914c9299
I also added a programmatic empty search results view and added it to tableview background, using the condition to show/hide the empty state view.
The empty state view also has a delegate to notify the viewcontroller when it’s pressed, so that a random state list is shown that users can select – https://github.com/iAkashlal/Weather-app/commit/eddcb09fd99d212c12ac4c4f045b2ee919420290
And the app now looks complete – Albeit a single page
Adding the weather detail page
On tapping any city entry, a detail page to be shown which would be a simple stackview containing all text labels embedded inside a viewcontroller. Since we are following the MVVM pattern, the detail page will have an associated viewModel, which will query OpenWeather api for weather forecase for the chosen location.
I went ahead and added a quick UI mockup along with a file structure to facilitate MVVM architecture – https://github.com/iAkashlal/Weather-app/commit/46739064a4704e1200c6d0ed47320ea41271ec2d (you can download the project by going to the commit link > Browse Files > Code > Download Zip
Unlike the search list module, I used a delegation design pattern to connect the viewModel to viewController which is ‘technically’ better than using closures.
To be able to add the forecase API, we will use Latitude and Longitude obtained from search results screen to fetch forecast using WeatherAPI, however we are yet to add the weather forecast API call, which is dead simple thanks to moya. From the documentation, https://openweathermap.org/current, its a GET request with 2 parameters, which when translated to code,
This API can now be used in weather detail page viewModel, along with skeleton loaders – https://github.com/iAkashlal/Weather-app/commit/67dca1d486f211ff62d2fa0d90a695b6d63ae819
Connecting search list to weather detail page
We have two separate pages, where the detail page now depends on list page. It needs a latitude and longitude to fetch and show weather data.
For now, creating a detail viewcontroller instance on searchVC’s didSelectRowAt method, and pushing it onto the navigation stack to check if everything is working fine.
After that’s done, I realised a few things where wrong due to which the detail page wasn’t decoding from response. Fixing that up, we now have an almost fully functioning two pager weather app – Link to code – https://github.com/iAkashlal/Weather-app/commit/bf025d32770ff571ed38d59c23dc595fa8709600
Updating Navigation with coordinator design pattern
While a simple app like this doesn’t really need a coordinator, our technique of showing the detail screen from the list vc makes the design coupled.
Also, the first viewcontroller is now responsible for creating the second viewcontroller and presenting it, which is considered a bad practice. We have already moved the business logic to the viewModel, now it’s time to move the navigation logic to a coordinator.
Start by creating a protocol ‘Coordinator’ specifying all coordinators to have a navigation controller, an array of child coordinator and a start function which sets up the base viewcontroller.
import UIKit
protocol Coordinator {
var navigationController: UINavigationController { get set }
var childCoordinators: [Coordinator] { get set }
func start()
}
Add a concrete implementation of this coordinator which we will be using to load our viewControllers.
import UIKit
final class RootCoordinator: Coordinator {
var navigationController: UINavigationController
var childCoordinators = [Coordinator]()
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let searchVC = SearchVC.loadFromNib()
navigationController.pushViewController(searchVC, animated: false)
}
}
For our weather application to begin using this coordinator, we can make some changes to the AppDelegate
Since coordinators are to be the route for navigation, all viewcontrollers should have access to the coordinator, just like they have access to their viewmodels. Adding a coordinator variable in both viewcontrollers, we can pass in coordinator when instantiating the viewcontroller.
Now the search coordinator has access to the rootcoordinator, we can add in a function in rootcoordinator that instantiates and presents a weather detail viewcontroller so that searchVC is no longer strongly coupled with weather detail viewcontroller.
func presentDetailFor(location: SearchResultModel) {
let weatherDetailVC = WeatherDetailVC.loadFromNib()
weatherDetailVC.setLocation(with: location)
weatherDetailVC.coordinator = self
navigationController.pushViewController(weatherDetailVC, animated: true)
}
and calling this function and passing the location from searchVC, we are able to present the proper screen without the first viewcontroller having much information about the next viewcontroller.
Link to code – https://github.com/iAkashlal/Weather-app/commit/0478f74ceddfc3b0aa1602543ea0125cd836f3a0
Some networking bugfixes
When the user is typing letter after another, one network call is made on every letter type. Previous network calls become redundant on every network call, which needs to be discarded. Since moya requests are cancellables, we can collect them in an array and call .cancel() on each of them before new requests are made.
Also, if search text is empty, we don’t need to make the network call, hence I added a patch to handle it locally.
These fixes are available in https://github.com/iAkashlal/Weather-app/tree/bugfix/cancelling-unnecessary-calls
Also added weather images to the detail page to complete the application, feature wise. Code for that addition is available https://github.com/iAkashlal/Weather-app/tree/feature/weather-images