Developing iOS apps with MVC: A practical example

Last week, I explored what the design pattern “Model-View-Controller” (MVC) is and created a playground to demonstrate the idea. But what does this look like a full-fledged application with API calls and Storyboards and design requirements? This week, we’ll see how that plays out.

Design

Imagine you’ve got a new project to do, and design have given you what to implement. It’s a news app which hooks up to NewsAPI, which generously allows you to request news sources and articles for free. Here’s what your designer hands off to you:

The application is very simple, but it does everything you need a news reader to do:

  1. We have a launch screen
  2. A list a square icons for each news source
  3. A table of news articles, with images, the author, and the title
  4. A web view with the full article

Planning

How does this design decompose into MVC components?

View Controller

Let’s start with view controllers, as view controllers are almost 1:1 with a designer’s screens. In this case, the launch screen will be done simply in our LaunchScreen.storyboard file, we’ll have a SourcesViewController, an ArticlesViewController, and then we’ll reach for the pre-built SFSafariViewController for the last view.

View

What views will each of these view controllers have? The launch screen will only need a UILabel, there isn’t much to do there. The SourcesViewControllerprobably needs a UICollectionView, with a custom SourceCollectionViewCell that we’ll make. The ArticlesViewController would be best as a UITableView, along with a custom ArticleTableViewCell.

Model

To determine what our model layer looks like, we cannot go from the designs, as it’s not a visual component, but rather, we must check the API documentation from NewsAPI.

Let’s start with the documentation for the response from the source’s API call:

status (string) – If the request was successful or not. Options: ok, error. In the case of error a code and message property will be populated.
sources (array) – A list of the news sources and blogs available on News API.

id (string) – The unique identifier for the news source. This is needed when querying the /articles endpoint to retrieve article metadata.
name (string) – The display-friendly name of the news source.
description (string) – A brief description of the news source and what area they specialize in.
url (string) – The base URL or homepage of the source.
category (string) – The topic category that the source focuses on.
Possible options: business, entertainment, gaming, general, music, politics, science-and-nature, sport, technology
language (string) – The 2-letter ISO-639-1 code for the language that the source is written in.
Possible options: en, de, fr
country (string) – The 2-letter ISO 3166-1 code of the country that the source mainly focuses on.
Available options: au, de, gb, in, it, us
sortBysAvailable (array) – The available headline lists for the news source. The possible options are top, latest and popular.

top Indicates this source can return a list of headlines sorted in the order they appear on the source’ homepage.
latest Indicates this source can return a list of headlines sorted in chronological order, newest first.
popular Indicates this source can return a list of its current most popular headlines.

There’s no one right way to do this, but roughly, our models are going to follow the structure and variables of our API. What I read here is that we’re going to need a SourcesResponse model with the status variable and the sources as an array of Source models. A Source model has an ID, a name, a description, a URL, a category (probably best to make this an enum, as it has a closed set of possible values), a language, and a country (again both these last ones are best as enums).

Let’s check out the documentation on the articles response:

status (string) – If the request was successful or not. Options: ok, error. In the case of error a code and message property will be populated.
source (string) – The identifier of the source requested.
sortBy (string) – Which type of article list is being returned. Options: top, latest, popular.
articles (array) The list of headline metadata requested.

author (string) – The author of the article.
description (string) – A description or preface for the article.
title (string) – The headline or title of the article.
url (string) – The direct URL to the content page of the article.
urlToImage (string) – The URL to a relevant image for the article.
publishedAt (string) – The best attempt at finding a date for the article, in UTC (+0).

Again, there’s no one right or settled way to turn JSON into model objects, but what I see here is that we’re going to need a ArticlesResponse model with an array of Article objects, which each have an author, description, title, url, urlToImage, and published at, all as Strings.

So in all, if we give a Swift struct to each entity and a Swift enum to each closed set of values, we’ll end up with these models:

  • Article
  • ArticlesResponse
  • Source
  • SourcesResponse
  • Category
  • Country
  • Language
  • Sort

Visualisation

How does this look like in the graphs we made last week? Let’s take the ArticlesViewController as an example:

But of course, this single MVC group is going to have to work with all the others in the full implementation. That looks something like this:

But this is beginning to get messy, so let’s separate all of our files into their respective layers:

Development

Now that we have a plan, it’s time to set it in motion. Fortunately, not only are we working with a designer, but an iOS architect that has created a shell of project for us using the information above, but it’s going to be up to us to add the implementation. You can download that shell here. You’ll note that it includes an API client pre-built for us, and the creation of an API client is outside the purview of our MVC discussion but it’s a very worthwhile topic I’ll devote time to in a latter post.

View

First, you should build your Storyboard to spec. First, add the label to the LaunchScreen.storyboard file. You’ll need one UINavigationController and two UIViewControllers, one with a UICollectionView and one with a UITableView. The collection view and table view will need a custom cell each, with an image view and labels each. Refer to the design to get this just right.

Now that we have these built in Storyboard, we’ll need to create corresponding source files for each cell type. Here’s what that SourceCollectionViewCell should look like:

class SourceCollectionViewCell: UICollectionViewCell, ReuseIdentifiable {
    static var ReuseIdentifier : String { return "SourceCollectionViewCell" }

    @IBOutlet weak var iconImageView: UIImageView!
    @IBOutlet weak var nameLabel: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        prepareForReuse()
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        iconImageView.image = nil
        nameLabel.text = ""
    }
}

You should use this as an example to build ArticleTableViewCell, which is much the same in form. You should also use this opportunity to connect the table view and collection view as IBOutlets to their respective UIViewController and set their delegates and data sources to, also, their respective view controller.

Model

Now that we have the easy stuff out the way, let’s build our first model. The article is a good place to start, I suspect it looks something like this:

struct Article {
 let author: String?
 let description: String?
 let title: String?
 let url: String?
 let urlToImage: String?
 let publishedAt: String?
}

We’re choosing a struct because it is a lighter entity in the Swift language than a class, but really there are reasons to go either way. These models do not exist in a vacuum however, we need to transform these models from JSON into structs. Fortunately, our architect has given a convenient means of doing so with the JSONTransformable protocol, which is very simple:

protocol JSONTransformable {
 init(json: Any)
}

When our model structs conform to this, we’ll add the implementation for converting them from JSON represented as an “Any”, which could be an array or dictionary depending on our API responses. What does our struct look like with this implementation?

struct Article: JSONTransformable {
    let author: String?
    let description: String?
    let title: String?
    let url: String?
    let urlToImage: String?
    let publishedAt: String?

    init(json: Any) {
        let jsonDictionary = json as? [String : Any]
        author = jsonDictionary?["author"] as? String
        description = jsonDictionary?["description"] as? String
        title = jsonDictionary?["title"] as? String
        url = jsonDictionary?["url"] as? String
        urlToImage = jsonDictionary?["urlToImage"] as? String
        publishedAt = jsonDictionary?["publishedAt"] as? String
    }
}

And with a little magic from Swift generics in our API client, these can now be converted from their JSON representations returned from the API. Let’s give the Articles response model the same treatment:

struct ArticlesResponse: JSONTransformable {
    let status: String?
    let articles: [Article]?

    init(json: Any) {
        let jsonDictionary = json as? [String : Any]
        status = jsonDictionary?["status"] as? String
        if let articlesJSONArray = jsonDictionary?["articles"] as? [[String: Any]] {
            var anArticles: [Article] = []
            for sourceJSONDictionary in articlesJSONArray {
                anArticles.append(Article(json: sourceJSONDictionary))
            }
            articles = anArticles
        } else {
            articles = nil
        }
    }
}

Notice how this initializer uses the JSON-based initializer from our Article class, meaning that when we request this response from our API, this initializer will cascade through all the article JSON entries to build structs for each. We’ll need to do the same for both the Source and SourcesResponse models, which is left as an exercise to the reader! (You’ll also find it as a download at the end.

But the Article and Source models aren’t the only types of models we have, there are also the enum models for the Language and Country, etc. Let’s see how one of those looks:

enum Category: String {
    case business = "business"
    case entertainment = "entertainment"
    case gaming = "gaming"
    case general = "general"
    case music = "music"
    case politics = "politics"
    case science = "science-and-nature"
    case sport = "sport"
    case technology = "technology"
}

This has the typed name on the left and the value on the right side of the equals sign, making it easy to parse these using the raw value initializer of these objects. You’ll see this in the initializer for Source. You should use this as an example to create the models for Country, Language, and Sort.

View Controller

Now that we have our models finished and our views finished, it’s time to write the glue code between our two layers: the view controllers. First, let’s get our Source view controller working:

class SourcesViewController: UIViewController {

    let api = NewsAPI()
    var sources: [Source] = []

    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout!

    override func viewDidLoad() {
        super.viewDidLoad()

        api.load(.sources(categories: nil, languages: nil, countries: nil)) { [weak self] (response: SourcesResponse?, error:Error?) in
            self?.sources = response?.sources ?? []
            self?.collectionView.reloadData()
        }
    }
}

extension SourcesViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return sources.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell : SourceCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath)
        cell.iconImageView.setIconImage(from: sources[indexPath.row].url)
        cell.nameLabel.text = sources[indexPath.row].name
        return cell
    }
}

extension SourcesViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let numberOfCellsPerLineFloat: CGFloat = 2
        let itemWidthWithoutConsideringInteritemSpace = collectionViewFlowLayout.collectionViewWidthWithoutInsets / numberOfCellsPerLineFloat
        let numberOfInteritemSpaces = (numberOfCellsPerLineFloat - 1)
        let amountOfInteritemSpacePerCell = (collectionViewFlowLayout.minimumInteritemSpacing / numberOfCellsPerLineFloat)
        let itemWidth = itemWidthWithoutConsideringInteritemSpace - (numberOfInteritemSpaces * amountOfInteritemSpacePerCell)
        return CGSize(width: itemWidth, height: 208)
    }
}

Notice how the view controller has access to both the model and view, but neither the model nor the view has access to each other. This property results in one of the big benefits of MVC, in that if either your model or view changes, as I assure you they will, you minimize the amount of code you will need to re-write because your models and views are completely independent, and in fact, could be re-used in other parts of your applications or even other applications.

But this isn’t quite the whole picture, let’s get our ArticlesViewController working:

class ArticlesViewController: UIViewController {
    let api = NewsAPI()
    var source: Source!
    var articles: [Article] = []
    
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        title = source.name

        api.load(.articles(source: source, sort: nil)) { [weak self] (response:ArticlesResponse?, error:Error?) in
            self?.articles = response?.articles ?? []
            self?.tableView.reloadData()
        }
    }
}

extension ArticlesViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return articles.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: ArticleTableViewCell = tableView.dequeueReusableCell(for: indexPath)
        cell.authorLabel.text = articles[indexPath.row].author
        cell.previewImageView.setImage(from: articles[indexPath.row].urlToImage)
        cell.theTitleLabel.text = articles[indexPath.row].title
        return cell
    }
}

If you try to transition between the two view controllers by selecting a source, you will notice a crash. That’s because we need to pass the selected source from the Source view controller to the Articles view controller in it’s prepareForSegue implementation (Be sure to add an identifier in your Storyboard, I also chose to represent my Segues as an enum).

class SourcesViewController: UIViewController {
    /* omitted to save space */
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        switch segue.identifier ?? "" {
        case Segue.sourcesToArticles.rawValue:
            let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first ?? IndexPath(item: 0, section: 0)
            let destination = segue.destination as! ArticlesViewController
            destination.source = sources[selectedIndexPath.row]
        default:
            ()
        }
    }
}

This should result in a successful transition from one view controller to the next, but when you select an article from the table view of the Articles view controller, unfortunately we run into another crash. This is because we need to add the implementation of UITableViewDelegate’s didSelectItemAtIndexPath to our Articles view controller. So let’s take a look:

extension ArticlesViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let article = articles[indexPath.row]
        if let url = URL(string: article.url ?? "") {
            let svc = SFSafariViewController(url: url)
            present(svc, animated: true, completion: nil)
        }
    }
}

Now, if we run it, it should push a web view controller when we do select an article.

Final Product

Here’s a video of what this app will look like with everything finished!

It looks great! I’m sure our designer will be proud!

Conclusion

We’ve seen how you can take a design and decompose it into views and view controllers, take an API spec and decompose it into model entities, and use that plan to implement an iOS app in Swift. To see some of the benefits this approach has and the arguments for it, I recommend checking out my last post on this topic. You should also know that this approach isn’t the only right way to develop in Swift for iOS or even nearly perfect. But it is the official design pattern for iOS and for good reason: it’s a reasonable place to start. In a forthcoming post, we’ll explore adding view models to this example. I’ll also write a post about the considerations in designing an API client. If you have any questions, feel free to add a comment below, reach out to me, or if you happen to be reading this before September 20th, 2017, come check this out live at the Noble iOS Meetup. Here’s the full file for those that want to check out the full working version.

Leave a comment

Your email address will not be published. Required fields are marked *