The app is written in Swift 3 and has a minimum deployment target of iOS 10. It has not been optimized for iPad.
CocoaPods is used as a dependency manager for the application. Before running the application make sure CocoaPods is installed on your machine. You can install/update CocoaPods by running the command sudo gem install cocoapods
in Terminal. Once CocoaPods is installed navigate to the root directory of the project in Terminal and run pod install
.
This is a very basic Twitter client implemented without the real Twitter API, OAuth or custom UI controls. There are 3 main screens to the app:
Users can sign up and/or log in using an email and password form. If an account exists with the entered email and password combination, the user will be logged in, otherwise a new user account will be created provided the credentials pass form validation. The email must be a valid email address and the password must be at least 6 characters in length. The user will be logged in automatically on subsequent launches of the app unless they log out.
Once the user is logged in, the app will display a feed of cached tweets and attempt to load new tweets. Reloading the feed by swiping down to refresh will cause the feed to attempt to fetch newer tweets from the provider. This screen also contains a log out button and a compose (+) button.
On the compose screen, the user can compose a new tweet to post. The content of the tweet is a string between 1 and 140 characters in length. From this screen the user can either cancel or post the tweet. Both cases return the user to the tweets screen. If a new tweet was posted it will be added to the top of the feed.
The application was developed using a MVVM (Model-View-ViewModel) pattern and a functional reactive programming library called RxSwift. I chose to use a reactive library because it helps eliminate state and provides better local reasoning, ultimately leading to cleaner and more maintainable code written faster with fewer bugs. I chose RxSwift in particular because it has similar implementations in many other languages (see ReactiveX site for more) so it would be ideal for cross platform development. I prefer MVVM over MVC in iOS because it allows for better separation of concerns. In iOS, the view (V) and controller (C) are both managed by view controllers. This leads to massive view controllers. MVVM does a better job separating presentation logic from business logic. There is a 1-1 relationship between view controllers <–> view models and cells <–> view models.
I divided the app into 3 main sections:
Login.storyboard
: Log in and sign up GUI.User.swift
: TheUser
class represents a user in the Realm database.LoginViewController.swift
: The view controller binds to theLoginViewModel
inputs and outputs.LoginViewModel.swift
: The view model maps the inputs into outputs.LoginProvider.swift
: Defines aLoginProviding
protocol that all login providers must conform to. This protocol has an associated type calledParameter
which defines what the login provider requires as input. I did this because I figured different login providers (e.g. username/password, Facebook, Twitter, etc.) would require different inputs. This way a login provider can have anything as an input (e.g. set of credentials, key, etc.) take while maintaining type safety. This file also contains a type erasedLoginProvider
calledAnyLoginProvider<T>
.LocalLoginProvider.swift
: Defines a structLoginCredentials
to encapsulate an email and password. Contains an implementation ofLoginProviding
with aParameter
ofLoginCredentials
.
Tweets.storyboard
: Tweet feed GUI.Tweet.swift
: TheTweet
class represents a tweet in the Realm database.TweetsViewController.swift
: The view controller binds to theTweetsViewModel
inputs and outputs.TweetsViewModel.swift
: The view model maps the inputs into outputs.TweetCell.swift
: The cell binds to theTweetCellViewModel
inputs and outputs.TweetCellViewModel.swift
: The view model maps aTweet
into outputs to update the cell UI elements.TweetProvider.swift
: Defines protocolsTweetFetching
,TweetPosting
andTweetProviding
to define the requirements of the various tweet related operations. All protocols require aUser
as input.LocalTweetProvider.swift
: Contains an implementation ofTweetFetching
,TweetPosting
andTweetProviding
using random data.
Compose.storyboard
: Compose GUI.ComposeViewController.swift
: The view controller binds to theComposeViewModel
inputs and outputs.ComposeViewModel.swift
: The view model maps the inputs into outputs.
The Cache was implemented using Realm – a relational mobile database. All data flows through the Cache. This means that data providers must update the Cache and the Cache updates the UI. This was done to create a funnel and standardize the way the UI is updated. The implementation of the Cache also uses NSUserDefaults
to persist a user across app launches and the Keychain to securely store user passwords. The passwords are not stored in plain text in the Realm database. They are stripped upon saving and injected upon retrieval.
There are 2 model objects:
User |
---|
email: String |
password: String |
Tweet |
---|
id: String |
user: User |
message: String |
date: Date |
- KeychainSwift: Keychain wrapper to simplify reading from and writing to the Keychain
- Realm: Mobile database used for caching
- RxCocoa: Reactive wrapper for Cocoa
- RxGesture: Reactive wrapper for view gestures
- RxKeyboard: Reactive wrapper for keyboard
- RxRealm: Reactive wrapper for Realm
- RxRealmDataSources: Reactive extensions for binding Realm data to table and collection views
- RxSwift: Core functional reactive programming library
- RxSwiftExt: Additional operators for RxSwift
- RxTest: Testing framework for RxSwift
- SwiftLint: A tool to enforce Swift style and conventions (runs as a build phase)
- SwiftRandom: Random data generator
I wrote several unit tests for the models (UserTests
, TweetTests
, CacheTests
, LoginCredentialsTest
, LocalTweetProviderTests
) to test the various operators that are defined for each.
I wrote several integration tests for the view models (LoginViewModelTests
, TweetsViewModelTests
, ComposeViewModelTests
) to test the mapping of the various inputs to the outputs. For these tests I used the TestScheduler
class in RxTest
to test observable sequences in virtual time.
- A random delay is added to most data provider actions to simulate a network delay.
- I did not implement any reachability detection in the app meaning log in and sign up can occur at any time regardless of network connectivity. I figured the data providers would be responsible for returning appropriate errors. I did not include such feedback in my local data providers.
- A common pattern in my view model outputs was to have an observable for the successful result (e.g.
composeViewModel
inLoginViewModel
) and an observable for the errors. This was because inflatMap
ing the inputs to the outputs, if the observable returned in theflatMap
was to error, the error would not be propagated up the stream. Instead, the sequence would complete and further inputs would have no effect. To circumvent this, I use thematerialize()
function ofRxSwiftExt
to convert the stream into a stream of events. I then use theelements()
anderrors()
properties to forward the events to the correct streams. Another benefit is that all the errors get propagated to one observable and can be handled together (e.g. in theTweetsViewModel
both loading newer tweets and logging out can error).
Cole Dunsby, coledunsby@gmail.com