Sherpany Code Challenge
by Nuno Grilo
- Navigate to repo
- Clone locally using
git clone https://github.com/nfgrilo/sherpany-challenge.git
- Open
Sherpany Posts.xcodeproj
in Xcode - Build & Run
- Enjoy!
The application was designed with the simple but effective Coordinator Pattern. The Posts app is making use of 4 coordinators:
MainCoordinator
: the app coordinatorPostsCoordinator
: the post list coordinator for the master viewPostDetailsCoordinator
: the post details coordinator for the detail viewFullscreenPhotoCoordinator
: the coordinator for the full-size image viewer
A coordinator is an object that manages one or more view controllers. They take all the driving logic out of view controllers, moving it one layer up:
Coordinator
->UIViewController
->UIView
Similarly to views and view controllers, coordinators exist in tree structures. This allows to push down tasks into child coordinator (when parent coordinator grows), which helps to easily manage complexity giving space for growth. A child coordinator can create view controllers, wait for them to complete, and signal up when done. When a coordinator 'finishes', it cleans itself up, popping off whatever view controllers it has added, and then uses delegates to get messages back up to its parent.
A slight variation of the "standard" Core Data stack that comes for free with NSPersistentContainer
was used:
mainManagedObjectContext
:- main thread context, read-only, have persistent store as parent, automatically merge changes from persistent store and has merge policy set to
mergeByPropertyStoreTrump
- main thread context, read-only, have persistent store as parent, automatically merge changes from persistent store and has merge policy set to
backgroundManagedObjectContext
:- background context, used to perform long/write operations, have persistent store as parent, merge policy set to
mergeByPropertyObjectTrump
- background context, used to perform long/write operations, have persistent store as parent, merge policy set to
These are set on DataController
.
Merging of fetched data with Core Data persisted data is handled the following way (search "Requirement #5
" in Xcode):
- Modeled all entities with an
id
(unique) constraint. - Set appropriate Core Data merging policy by setting the
mergePolicy
of the managed object context toNSMergeByPropertyObjectTrumpMergePolicy
(so that, data in memory takes precedence over persisted data). - Remove any orphan
Post
orUser
(although it is not possible to add any of these from the app), which will cascade delete related entities when appropriate. - Persisted data is then iterated and updated with fetched data in
O(n)
time andO(n)
space complexity.
PS: if a post is requested to be removed while a data refresh operation is in place (fetching & merging takes some time), the post removal will be postponed (so the post will actually be removed, and after the data refresh completes).
There are two groups of model objects in the app:
- Managed model objects (handled by Core Data) (
ManagedPost
,ManagedUser
,ManagedAlbum
andManagedPhoto
) - Simple model objects (
Post
,User
,Album
andPhoto
)
This simpler model object consists of Swift structs, providing a simple and safe way to achieve immutability and thread safety throughout the app. All model accesses are made through the ModelController
class, which only return simple model objects.
The API resources were defined using a protocol-oriented approach, and are accessible from the controller APIController
:
APIResource
: describes each API resource by its method and URLAPIRequest
: provides request related functionality (network fetch)APIResponse
: represents each model resource (asCodable
)APIError
: describes any error occurred during API access
Photo (image) fetching is made by the PhotoController
controller.
Each photo request translate into a PhotoController.Task
being added to a queue, so that no multiple connections to the same image resource are ever made. Additionally, tasks have different priorities depending on the context (prefetching, fetching for visible cell, cancelling prefetching, full-sized image fetch), so dequeuing always pick the higher priority tasks.
When queueing (and dequeuing) tasks, thread-safety is accomplished by using a concurrent queue with barrier (faster than a serial queue). Read accesses can be asynchronously, while writes are made synchronously with barrier, so that once a "write" is placed on the queue, it is only executed when there are no more "reads" in the queue.
Network requests are made on a concurrent queue. However, because we could generate too many simultaneous requests to the same server, the number of network connections is limited to 10
(PhotoController.httpMaximumConnectionsPerHost
).
PS: Please note that, for the purpose for the code challenge, the app is making use of the URLSessionConfiguration.ephemeral
in-memory configuration. For a real-life application, this should be either URLSessionConfiguration.default
or a custom one, so it could make use of disk caching, etc.
This is handled by MainCoordinator
. This coordinator will then create & delegate control to PostsCoordinator
and PostDetailsCoordinator
to handle their master and details view controllers, respectively. Each view controller is then loaded into an UISplitViewController
.
The post list is handled by the PostsCoordinator
. This will create the view controller, its data source and the search controller. The protocol PostSelectedDelegate
is used to signal the observer (PostDetailsCoordinator
) that the post selection has changed.
PostsCoordinator
is a delegate of ModelController
(via ModelControllerDelegate
) and it's notified when new data becomes available (after fetching & merging data with persisted one) to refresh the post list.
Post title, body and user albums are handled by PostDetailsCoordinator
, which shows either NoPostDetailsViewController
if no post is selected, or PostDetailsViewController
otherwise.
A UICollectionView
is used to show all the post details:
- post title and post body: implemented as the first collection view section header (
PostDetailsHeaderView
) - album titles: implemented as collection view sections (
PostAlbumHeaderView
) - photos: implemented as collection view items (
PostAlbumCollectionViewCell
)
A custom collection view layout was implemented (PostAlbumCollectionViewFlowLayout
) in order to have top-aligned photos.
PostDetailsCoordinator
implements PostSelectedDelegate
to be notified of post selection changes. Whenever a post is selected, the coordinator will instruct its view controller to refresh its contents. In case we have just refreshed all data (fetch & merge), it will re-select the same post and restore the viewing context (scrolling position, albums collapsed state, and if a full-sized photo is being shown).
FullscreenPhotoCoordinator
handles the view controller responsible for showing full-sized images. It is a child coordinator of PostDetailsCoordinator
. The coordinator will then make use of FullscreenPhotoViewController
to display full-sized photos. User can navigate back using the navigation bar back button or tapping the photo.
Network requests to full-sized photos are always made with higher priority than any thumbnail.
This project has no 3rd party libraries dependencies.
Develop an iPad application that conforms with the requirements below.
Please note that you can find code related with a specific requirement by searching "Requirement #XX
" on Xcode.
- ✅ Set the navigation bar title to “Challenge Accepted!”
- ✅ Use Swift
- ✅ Fetch the data every time the app becomes active:
- Posts: http://jsonplaceholder.typicode.com/posts
- Users: http://jsonplaceholder.typicode.com/users
- Albums: http://jsonplaceholder.typicode.com/albums
- Photos: http://jsonplaceholder.typicode.com/photos
- ✅ Persist the data in Core Data with relationships
- ✅ Merge fetched data with persisted data, even though the returned data of the API currently never changes. See (6).
- ✅ Our UI has a master/detail view. The master view should always be visible and has a fixed width. The detail view adapts to the space available depending on orientation.
- ✅ Display a table of all the posts in the master view. For each post display the entire post title and the users email address below the title. The cell will have a variable height because of that
- ✅ Implement swipe to delete on a post cell. Because of (4) the post will appear again after the next fetch of the data which is expected.
- ✅ Selecting a post (master) displays the detail to this post. The detail view consists of the post title, post body and related albums.
- ✅ The creator of this post has some favorite albums that we want to display along the post. An album consists of the album title and a collection of photos that belong to the album.
- ✅ The photos should be lazy-ly loaded when needed and cached. If the photo is in the cache it should take that, otherwise load from web and update the photo cell.
- ✅ In general, provide UI feedback where you see fit.
Please note that you can find code related with a specific bonus by searching "Bonus #XX
" on Xcode.
- ✅ It would be nice to be able to show/hide the photos of an album. All albums are collapsed in the default state.
- ✅ Because the collection of photos can get quite long, we would like the headers to stick to the top.
- ✅ Include a search bar to search in the master view and provide live feedback to the user while searching.
Extra Bonuses 👍
- ✅ UI State restoration when merge of fetched and persisted data ends (selected post, scrolling position, albums collapsed state, ...)
- ✅ Tapping a photo (thumbnail) shows a new view with the full size photo
- ✅ Dynamic Type support: UI adapts to text size changes in Settings app (General > Accessibility > Larger Text)
- ✅ Unit Tests for controllers:
APIController
,DataController
,ModelController
andPhotoController
-
- ✅ Unit Tests for coordinators:
PostsCoordinator
- ✅ Unit Tests for coordinators: