There was no requirement for supporting a minimum version of iOS, so I've tried to create a user experience that gets the most out of the most recent and modern Apple APIs. I've created the project with iOS 15 as the target deployment version. The App has SwiftUI at its core, but of course, if there was a requirement to provide support for iOS 12 or below, an implementation with a mix of SwiftUI (for iOS 13+) and UIKit (for iOS 12 and below) could be possible.
The App features the custom navigation control NavigationBar
. This was required so we could animate the control when the user scrolls. GeometryReader
and ScrollPreferenceKey
were used to detect user scrolling, and AnimatableFontModifier
was used to animate the title font after scrolling.
The App is also ready for using in dark mode. You can preview views by using .preferredColorScheme(.dark)
or in Simulator --> Features --> Toggle Appearance.
For this prototype, navigation into a story has been implemented for the Featured Story only. This has been implemented with .matchedGeometryEffect
techniques I've learned earlier this year. These enable elements of the view that are matched between both source and destination view to align and produce a smooth transition effect when animating. I've animated the transition so that it gives the effect that we are maximising the feature story into its own full screen mode.
After you open the featured story, different sections of the story will appear at separate times (see var appear: [Bool]
in StoryView
), which gives a chain animation effect. You will be able to drag down to see a scaling effect in the background as you drag.
When you dismiss/close the featured story, you will be taken back to the main view in the app. At the moment there is a bug where the app doesn't come back to the original state it was before showing the featured story, and stays in a mode waiting to receive new stories, so you will need to drag down to request the stories again.
The section with the Latest stories does not navigate into details of a story, but this behaviour can be implemented in a similar way as the featured story, or by using other navigation methods such as a presentation sheet or a navigation link. If the item in the list is a web link, it will navigate to the website specified, however for this prototype the sample entries will navigate to DuckDuckGo
I've followed a layered approach where components are distributed across software layers. They communicate with each other when required.
- UI - This component starts on
ContentView
, as this is the view at the top of the hierarchy.ContentView
displays aStoryView
instance in full screen mode when the user taps on the displayedFeaturedStory
. TheStoriesView
is shown in the Latest section and usesStoriesViewItem
to display each story in the list orStoriesViewItem_WebLink
for web links. Future implementations of the App will require developing an advert view that can also be used by theStoriesView
list and can be filled with[Advert]
data provided by the presentation object. - Presentation - This layer is in charge of controlling and communicating changes between the data layer and the UI.
StoriesModel
is the main component of this layer. It usesObservableObject
as part of the Combine framework to communicate changes between layers. @Published property wrapper has been used as part of Apple's new reactive model to refresh data in the UI and in the data layer automatically. a Model object needs to be initiated with a specific implementation ofDecodeProviding
so that it connects to the correct data source.MediaMaker
implementations read information contained in data structures after the presentation class has accessed the data (viaDecodeProviding
implementations), and then they match properties from data structures to the presentation structures that conform toNewsRepresentable
to create specific objects of stories, web links or adverts. These will then be ready for using in the UI when required and can be read from instances of presentation objects (model classes). - Data - This layer contains the structures that hold data used by the presentation layer:
StoryData
andStoryDataFeed
. These structures resemble the properties found in the expected JSON response and thus are very basic and only conform toDecodable
and if the response is providing anid
, they will also conform toIdentifiable
. - Other components Data Providers are used to facilitate access to data. All the providers are based on the
DecodeProviding
protocol, as it is expected that all the providers will decode the data they access.func parseData<T: Decodable>() async -> T?
was declared as a generic so that it allows for writing the same method for parsing different types of objects, so we can retrieveStoryData
orStoriesDataFeed
and its inner[MediaItem]
from the network in a similar way. All the providers can also use the default implementation offunc decode<T: Decodable>(_ data: Data) throws -> T?
which was declared as an extension ofDecodeProviding
which provides a default behaviour for decoding all the structures and does not need to be different or modified for any of them.
URL creation occurs via extensions to URL
and URLCompoments
found in Extensions.swift. For sample data, I've created URLs using images retrieved from Lorem Picsum, which provides a great interface for prototyping and testing network access to images of different sizes. At the moment URLs are built using static strings found in Extensions.swift (see extension String
). You can edit the URL strings for the Sky server and then use URL.newsListURL
to create a valid URL that connects to a Sky server to retrieve live Cat news. This URL can be used by a network provider (e.g. In ContentView
you can declare @ObservedObject var storiesModel = StoriesModel(networkProvider: NetworkProvider(url: .newsListURL))
instead of the current model that connects to a file provider).
Error providers conform to the protocol ErrorProviding
and are used by the DecodeProviding
data providers to provide errors when trying to access data. Other areas of the app where a critical error has been found, use messages found in Extensions.swift. At the moment these areas use preconditionFailure
to stop the app in debug mode, as there is no specific requirement about what we should do for each error (e.g. display error messages to users or create a logging system and display default placeholder views to users)
Tests coverage for business logic and model data can be found in SkyCatNewsTests
.
Test coverage for UI behaviours and workflows can be found in SkyCatNewsUITests
.
I added additional test values in Extensions.swift. These are widely used in both test projects to validate data. This file needs to have Target Membership for all the projects so that it can be used by the test projects.
- Navigation back from a detailed story (
StoryView
) to the main view (ContentView
) is currently bugged because it does not load the content again. The user will need to drag down to refresh the view and load the content again. StoryView
displays a list of story paragraphs and images. At the moment the image is not shown and this implementation is not working ( a text placeholder that saysImage goes here
is shown instead)- There are a total of 5 FIXME locations in the App that will need changing to live server commands when the connections are ready.
- The UI Testing project still requires a lot of work. I could use query techniques such as
.descendants(matching: .staticText).allElementsBoundByIndex
ormatching: .image
amongst other to find elements and check that accessibility features are available. It is also possible to use accessibility identifiers to find theFeatureStory
view in the main UI and the close button inStoryView
to create a UI Testing workflow where we can performfoundElement.tap()
orapp.swipeDown()
to create automated interactions in the UI and validate that certain view elements are being displayed after these actions (e.g. to test that navigation is working) - The unit test project still needs additional testing functions to cover tests of the model objects
StoriesModel
andStoryModel
using certain conditions (e.g. with a mocked network provider, a file provider, or perhaps if we created another mocked provider that uses data stories in variables) - The App requires an internet connection to access media files located in Lorem Picsum. The image controls that show all the images are of type
AsyncImage
and use aProgressView()
as a placeholder. If the App is emulated from a device that has no internet access, theProgressView()
will show indefinitely. If the app was intended for showcasing within an environment with no network access at all, it might be useful to replace all the instances ofProgressView()
withinAsyncImage
placeholders with a localImage(systemName: "photo")
- To manage error handling correctly we need to replace all instances of
preconditionFailure
with either a logging system or a friendly message shown to the user. - As a prototype, each time we run the app, we are generating different updated times for stories and web links. These are sorted by updated, so the Latest section will show different results each time we run the app (i.e. web links and stories will be shown in different rows of the list each time we run the app).
- Although several measures used by the design (e.g. width, height, and padding sizes) have been extracted into static variables in extensions of
CGFloat
andDouble
, there are still UI components with design values that need extracting to this area.