Chapters

Hide chapters

Real-World iOS by Tutorials

First Edition · iOS 15 · Swift 5.5 · Xcode 13

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

2. Laying Down a Strong Foundation
Written by Renan Benatti Dias

Last chapter, you got a glimpse of PetSave and what you’ll build in this book. Now, you’ll start your journey by laying down a strong foundation while learning multiple topics to keep in mind while developing your apps.

In this chapter, you’ll learn about the many design decisions you may face while developing an iOS app and why it’s essential to think about how architecture affects your app as it grows.

More specifically, you’ll learn:

  • How to organize your project and use a layered architecture using feature grouping.

  • Why it’s key to ensure your code has high cohesion and low coupling.

  • How SOLID principles and design patterns can help you write better code.

  • What PetSave’s domain is and what the domain layer is for.

  • How to identify app features and devise a plan of attack.

By the end of this chapter, you’ll create domain models that represent the foundation of PetSave’s features.

Feature grouping

Open the starter project and expand the PetSave group. You’ll notice a few groups already in the project:

Project Navigator with PetSave groups
Project Navigator with PetSave groups

These groups define the scope of each part of the app and have everything that relates to it inside them.

  • Core: Contains shared code between all features. It may contain views, business logic and data used on each part of PetSave.

  • AnimalDetails: Holds the views and view model that compose a pet’s details.

  • AnimalsNearYou: Contains the views and view model for listing animals near the user’s location.

  • Search: A group for the models, view model and views to search for a pet.

By grouping your files like that, you create vertical layers that contain most of the code the enclosed feature needs. Making them primarily independent from other parts of your code.

Diagram showing horizontal grouping and another diagram showing vertical grouping
Diagram showing horizontal grouping and another diagram showing vertical grouping

This diagram shows the difference between the horizontal and vertical grouping. In the first approach, you have pieces of code grouped by their likenesses. For example, code for the network layer is together, and code for the model layer is together.

The second diagram shows code grouped by their use. So, you group code related to each other together. This type of organization, known as feature grouping, defines the borders of each part of the app, so you have the right code at the right place. It has a few advantages:

  • It becomes easier to navigate and find code as the group’s name already tells you what to expect from it.

  • You create groups with high cohesion by only including code related to the same purpose.

  • Makes it easier to create modular code.

As your project grows, it may become difficult to maintain code and find each part of the app. That’s why it’s essential to consider the project’s organization early on. Following an organizational pattern like this helps mitigate problems and keeps code with high cohesion and low coupling.

But what does it mean to have a code with high cohesion and low coupling? In the next section, you’ll learn why this is important and how using principles like SOLID and Protocol Oriented Programming (POP) helps you write better and more reusable code.

Software isn’t written in stone

Requirements change. You might be working with agile frameworks or more traditional methods, but the requirements of software change one way or the other.

Changes might happen during development or even after releasing the app. It might happen because a feature doesn’t quite work as you expected or the users want something else. When users and stakeholders get hands-on with your app, ideas come up, features change and requirements are removed or added.

App development is an iterative process: You release a version and gather feedback. Developers must always be open to change and listen to the stakeholders and their needs.

Creating code that is flexible will help you when requirements change. Keep this in mind while designing a system, and you’ll save tons of time later. High cohesion and low coupling are two concepts you should take into account to write software that’s easy to maintain.

High cohesion

High cohesion refers to the ability to keep related components of your code together. That means writing code that fits well together and follows the same purpose or domain.

For instance, consider an enum that’s responsible for the API’s paths:

enum APIRouter {
  case animalsNearYou
  case search

  var path: String {
    “/v2/animals”
  }
}

APIRouter contains only a single path that addresses both endpoints, animalsNearYou and search.

Imagine that from now on, the web API requires you to generate a token to make requests. You would need to update APIRouter to add this new path:

enum APIRouter {
  case animalsNearYou
  case search

  // New route
  case token

  var path: String {
    switch self {
    case .animalsNearYou,
      .search:
      return "/v2/animals"
    // New path
    case .token:
      return "/v2/token"
    }
  }
}

This code might not seem like much, but APIRouter now handles two different paths with different purposes. One deals with the animal’s requests, while the other deals with authentication.

This enum would keep growing out of proportion with every new feature. Not only would it not make sense anymore, but it would also be tough to maintain and lack focus.

Instead of having a single enum that handles all routes, splitting it into two enums with a single domain makes it easier to change and understand the responsibility of each:

enum AnimalsRouter {
  case animalsNearYou
  case search

  var path: String {
    "/v2/animals"
  }
}

enum AuthRouter {
  case token

  var path: String {
    "/v2/token"
  }
}

Now, each enum has its purpose, making your code more cohesive and organized. Maintaining code with high cohesion reduces complexity and increases maintainability and reusability.

Low coupling

Low coupling code, or even completely decoupled code, can work by itself, in any situation, without depending on other components. Take a look at the following class:

class AnimalsNearYouViewModel {
  let service = Service()

  func fetchAnimals() {
    service.fetchAnimals()
  }
}

AnimalsNearYouViewModel uses a service to fetch animals to display them in a list. This class depends on Service.

But, what would happen if requirements changed and you had to use the user’s location to fetch animals? You would have to rewrite Service to add this functionality or create a new Service for this feature. Even so, AnimalsNearYouViewModel is still dependent on a concrete type.

Instead of doing that, you can use Protocols to create an abstraction for AnimalsNearYouViewModel that can accept any type, as long as it conforms to that protocol:

protocol AnimalFetcher {
  func fetchAnimals()
}

class AnimalsNearYouViewModel {
  let service: AnimalFetcher

  init(service: AnimalFetcher) {
    self.service = service
  }

  func fetchAnimals() {
    service.fetchAnimals()
  }
}

AnimalsNearYouViewModel now depends on AnimalFetcher. It takes any type that conforms to this protocol instead of a concrete type.

You also create an initializer that expects a type that conforms to AnimalFetcher. This way, you can pass any type that conforms to the protocol instead of instantiating a concrete type as a property.

Following these practices is known as Protocol Oriented Programming (POP).

This abstraction lets you use this class in different contexts with different purposes. You can make it even easier to test AnimalsNearYouViewModel by creating a mock type, also known as a spy, that conforms to AnimalFetcher.

Using design patterns and software principles

Following Protocol Oriented Programming, SOLID principles and design patterns is a great way to keep high cohesion and low coupling.

Even though you should strive to follow best practices, it’s important to understand when to use them. Otherwise, you might over-engineer your code. Understanding why these practices exist and how they can help with different problems will help you know when to use them and when not to.

Created by Robert C. Martin, also known as Uncle Bob, SOLID is an acronym for these five principles:

  1. Single Responsibility Principle: A module, class or function should be responsible for a single purpose, focusing on a single task. This single focus helps create code that doesn’t grow out of proportion by solving the entire problem.
  2. Open/Closed Principle: Define modules, classes and functions open for extension but closed for modification. In other words, you extend its behavior without modifying its implementation.
  3. Liskov Substitution Principle: Replace classes with their subclasses without breaking the code. You can also apply this idea in Swift by using protocols where you can use another type as long as it conforms to the same protocol.
  4. Interface Segregation Principle: A module shouldn’t depend on requirements that it doesn’t use. Instead of creating a protocol that defines the whole behavior of a type, creating a different protocol that covers other use cases helps modules use only what they need.
  5. Dependency Inversion Principle: A module shouldn’t depend on external dependencies, but the module should define its requirements. It states that your modules should depend on abstractions, not other modules. For example, by following this principle, you’ll be able to test your networking module in isolation without having to depend on concrete implementation of other parts of the app.

These principles are the foundation of many design patterns and software architectures. Having a good understanding of them helps you better understand why some architectures are the way they are.

Note: If you want to learn more about SOLID principles, check out SOLID Principles for iOS Apps. To read more about Protocol Oriented Programming, check out Chapter 25, Protocol-Oriented Programming of our book Swift Apprentice.

Understanding these principles will help you handle any software challenge you may face. But, they’re not enough to create a great app. While they’ll help engineer code, you still have to understand what you’re building.

That’s where the domain comes in.

The app’s domain

Before coding any features, you first have to understand what kind of app PetSave is: its purpose, features and users. Essentially, the domain defines what kind of app you’re building.

The domain is a set of business logic and rules your app follows.

Usually, you would gather requirements for PetSave at the beginning of the software development cycle. Project stakeholders define these requirements that you then translate into features.

Understanding the app’s domain is key to building a successful app. When you understand the user’s problems, it’s easier to build features that solve them. When users feel understood, and your app is valuable, they’re more likely to use it again.

Understanding the domain layer

Business logic is an integral part of app development. It defines and drives features and the way your app behaves.

As your app becomes larger, decoupling your business logic into a separate layer can help keep your project tidy. The domain layer removes business logic code from views, leaving only layout building and presentation logic.

Since the domain defines models that most features share, you’ll find most of them in the Core group.

Project Navigator with the domain layer groups
Project Navigator with the domain layer groups

This is the first part of PetSave you’ll work on. Later, you’ll create your first model and prepare its mock data. But before that, you’ll lay out a plan on how to tackle PetSave required features.

Planning the app

It’s time to take a closer look at each feature. Don’t worry: You don’t have to think of every single detail now. But it’s essential to plan things like how you’re going to build each feature, the UI’s design and which feature you’re going to build first.

You’ll start by understanding what each feature tries to do. Then, you’ll lay out a simple UI and plan how data flows in that feature.

You’ll also use SwiftUI, Apple’s latest UI framework. It’ll help you build and iterate fast over user interface development.

Devising a plan of attack

There’s no single way to plan features. Planning a feature is all about understanding what problem you’re trying to fix. If the feature doesn’t help your user, it really might not be a feature.

You learned the importance of writing software that’s easy to change and scale. Now, you’re going to learn that planning a feature is just as important.

Identifying app features

To start, you need to identify the features you’ll build. PetSave has a closed scope, so you’ll work with features with defined requirements that won’t change as you develop the app.

Those features are:

  1. Animals Near You: The app displays a collection of pets for adoption near the user’s location.

  2. Search: Aside from browsing pets, users can also search pets by name and filter the results by age and type.

  3. Onboarding: A simple introduction to the app’s features. This is an essential feature because it’s the first thing users see when they open the app for the first time.

After you understand those features, you’ll layout a UI and the workflow of each feature.

Before you start planning those features, there’s one last thing to consider: how your app’s data flow will work.

Understanding view models

To follow best practices and keep views clear from presentation logic, you’ll use View Models to store view state and handle events from the user.

You learned that having a domain layer helps you keep domain-specific business logic decoupled from your views. View Models will help you bridge the view with your domain layer.

A View Model is a model that represents a view. It contains properties and methods to respond to all the data your view needs and handles presentation logic and data transformation. View Model is an excellent pattern for extracting events and states from views, letting them focus on UI building.

Model-View-ViewModel (MVVM) architecture relies heavily on using view models for presentation logic. It acts as an intermediate between the view and the data, fetching and transforming data to present in views.

PetSave follows most MVVM principles. By combining MVVM with SwiftUI’s state management, you get an architecture where views have a model to drive their state.

BUSINESS LOGIC Observes Send Events Updates View ViewModel Updates PRESENTATION LOGIC UI Model View State ViewModel
Diagram showing data flow between view, view model and model.

This architecture extracts dependencies from the view into a single source of truth, making your views clear of presentation logic so they can focus on UI building. It also makes testing state and data easier since view models are just regular objects.

Now, with the knowledge you’ve gained about view models and devising a plan of attack, you’ll break down the first feature, Animals Near You.

Animals Near You

This feature lets users scroll through a collection of nearby animals that are up for adoption. The Petfinder API takes the user’s latitude and longitude to fetch pets near them.

You’ll add the user’s real location later, in Chapter 12, “App Privacy”. For now, think about how to display a collection of pets that users can scroll through and tap over to see more information.

Animals Near You: Designing the UI

Right now, AnimalsNearYouView is empty with a TODO text. You’ll build each part of this feature later, in Chapter 5, “Building Features – Locating Animals Near You”. But it’s important to start thinking about how this feature will work.

SwiftUI is great for creating simple and efficient UI. It helps you build simple features faster so you can focus on your app’s cool and exciting features.

Displaying a collection of animals with SwiftUI is straightforward, so listing animals in a row seems like a great way to show the users pets for adoption.

Each row will show an animal with a picture, name, breed, type, gender and age to get users interested in that pet. This gives enough information upfront to hook the user into opening the details of that animal.

Now, this data has to come from somewhere, in this case, Petfinder’s API. That means users may have to wait a while for the request to complete. Users might get confused if they open the app and see a blank screen, so a loading indicator that says the app is fetching animals is essential to tell users the app is loading data.

Those are the basic ideas of how the UI should behave. In later chapters, you’ll learn about the SwiftUI views you’ll use to create this UI.

Animals Near You: Using view models

Now that you have an idea of the UI, you also have to think about how you’ll orchestrate fetching data and updating your views.

Here, you’ll use a view model to store the view state when the view is loading or if there are more animals to fetch. It’ll also respond to the user’s actions like refreshing and fetching animals when the view first appears.

Note: To learn more about using MVVM with SwiftUI, take a look at MVVM with Combine Tutorial for iOS.

Searching animals

Aside from listing animals near you, you’ll also get to build a search feature. Even though scrolling through animals might seem like a good idea while browsing for your next pet, some people might want to search for particular types of animals. They might want to find pets of a certain age, type or even name.

Giving multiple ways to find and search animals will help users on their quest to find their next best friend.

Searching animals: Designing the UI

In the starter project, SearchView is also empty with a TODO text.

A field where users can type a name and ask the app to search Petfinder’s API for animals with that name should be enough to help users search for animals. They’ll then see the results in a list, much like Animals Near You. This is the perfect opportunity to use SwiftUI’s greatest strength, reusing views. Later, in Chapter 6, “Building Features - Search”, you’ll learn to refactor code from AnimalsNearYouView to reuse it inside SearchView.

Also, you’ll build a small form where users can select the animal’s age and type. They can select from a close range of types so they can find a specific type of animal, like Cats, Dogs or even Horses. Also, users can try to find pets of a certain age like baby, young, adult or senior. You’ll have to add another field for users to select this.

While this already helps users find pets, this view might feel a bit empty when the user isn’t searching for anything. They might also not understand that they can search animals by type and age. So you’ll also build a view for suggesting types of animals the user may want to find.

Finally, if Petfinder’s API can’t find any animal with the requested name, type and age, you have to display a message to the user informing them.

Searching animals: Using view models

You’ll also use a view model to store the name users type and the type and age they select. Then you’ll take this information and search Petfinder’s API with it. The view model will also help you store the view state and handle all user interactions.

Onboarding

As for the onboarding flow of the app, you’ll build this feature in Chapter 7, “Multi-Module App”. In that chapter, you’ll learn about the benefits of modularizing by building a framework to manage the onboarding process of PetSave. You’ll also get to learn about the different dependency managers you can use while building an iOS app, like Swift Package Manager, and how to publish your framework in a remote repository to be later reused in different projects.

Understanding an offline-first approach

Besides all the functionality of both features, you’ll also implement an offline-first approach to fetching, storing and displaying data.

Instead of fetching data each time the user opens the app and waits for the request to finish, you’ll display data cached with CoreData. This cached data creates a pleasant user experience where users can see animals even while the app is still loading new data.

You’ll work on this in Chapter 4, “Defining the Data Layer - Databases”.

For now, you’ll set up a foundation for building these features.

Modeling domain models

Now that you understand what you’re building, you’ll start by modeling Animal, a key model in PetSave.

To start, take a look at the JSON that represents a single animal in the Petfinder API:

{
  "id": 52432090,
  "organization_id": "PA174",
  "type": "Cat",
  "species": "Cat",
  "age": "Adult",
  "gender": "Female",
  "size": "Medium",
  "coat": "Short",
  "name": "Kiki",
  // Other properties...
}

The PetFinder API provides several properties you may use in your app. You map those models from the API model to one of your domain’s models. You’ll find these models inside Core ▸ domain ▸ model.

Most of the time, apps have backend web services to handle data. Matching the backend models is usually enough for you to transform them from JSON to your domain.

Now, you’ll create a model that defines an animal in the project.

Creating the animal model

Inside Core, open the domain folder. Then expand model. Create a new Swift file inside the animal folder and name it Animal. Add the following code to the file:

struct Animal: Codable {
  var id: Int?
  let organizationId: String?
  let url: URL?
  let type: String
  let species: String?
  var breeds: Breed
  var colors: APIColors
  let age: Age
  let gender: Gender
  let size: Size
  let coat: Coat?
  let name: String
  let description: String?
  let photos: [PhotoSizes]
  let videos: [VideoLink]
  let status: AdoptionStatus
  var attributes: AnimalAttributes
  var environment: AnimalEnvironment?
  let tags: [String]
  var contact: Contact
  let publishedAt: String?
  let distance: Double?
  var ranking: Int? = 0
}

This model defines a type that represents an animal in your app’s domain. It has a couple of primitive properties for storing its traits, like name and species. The model also has a couple of custom types like Breed, which defines the animal’s breed, and PhotoSizes, to store URLs of each size of the animal picture.

You also use enums, like Coat, to store data. It can be one of the seven values:

enum Coat: String, Codable {
  case short = "Short"
  case medium = "Medium"
  case long = "Long"
  case wire = "Wire"
  case hairless = "Hairless"
  case curly = "Curly"
  case unknown = "Unknown"
}

Enums are great for enforcing type-safety when mapping values from JSON. Instead of mapping strings of the animal coat, you create an enum with pre-defined values.

You might have noticed that all models conform to the Codable protocol, even the enums. The Codable protocol is a typealias of Decodable and Encodable. Decodable lets you decode objects from other types of data representation, like JSON, to your models. Encodable lets you encode the models back into data.

You’ll use this to map the Petfinder API’s response to the domain models.

Value types vs reference types

Notice that most of the models are structs, and they are simple representations of data from Petfinder’s API. That’s because structs are value types.

Structs are very lightweight, so you can use structs to pass values around for a low memory cost. Each instance of a value type is a unique copy of that data, and Swift creates a copy of the value whenever you manipulate it.

Value type kiki kiki2 Animal Instance Animal Instance
Object Kiki pointing to a value of Animal. Another object, Kiki2, pointing to another value of Animal.

On the other hand, classes are reference types. Class instances maintain a reference to their data in memory, propagating changes to this data to other references.

Reference type kiki kiki2 Animal Instance
Both objects, Kiki and Kiki2, pointing to the same Animal instance.

Since models are simple representations of data, you’ll use structs to create them. Value types are great for this.

Note: Reference and Value types are a relatively advanced Swift topic. To learn more about it, check out Chapter 24, Value Types & Reference Types of our book Swift Apprentice.

Preparing mock data for the animal model

Before you move on to the data layer and start fetching data, there’s one last piece of work to do. You need to set up mock data for the animal model.

During development, you might want to run your app without depending on external data, test business logic or even iterate view design. Mocking data is a way to mitigate this dependency. It’s also great for creating unit tests for specific use cases.

You’ll use this data in your unit tests in later chapters like Chapter 4, “Defining the Data Layer - Databases”. When building UI, you’ll also use this data with CoreData to drive Xcode Previews.

For now, you’ll create an extension of Animal to load animals from a JSON file.

Inside the animal group, create a new file and name it AnimalsMock.swift. Add this code to the new file:

// 1
private struct AnimalsMock: Codable {
  let animals: [Animal]
}

// 2
private func loadAnimals() -> [Animal] {
  guard let url = Bundle.main.url(
    forResource: "AnimalsMock",
    withExtension: "json"
  ), let data = try? Data(contentsOf: url) else { return [] }
  let decoder = JSONDecoder()
  // 3
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  let jsonMock = try? decoder.decode(AnimalsMock.self, from: data)
  return jsonMock?.animals ?? []
}

// 4
extension Animal {
  static let mock = loadAnimals()
}

This code:

  1. Creates AnimalsMock that represents a response from the Petfinder API and makes it conform to Codable.

  2. Creates a function that loads AnimalsMock.json and tries to decode it to an object of AnimalsMock. Then it returns the array of animals inside that object.

  3. Automatically converts keys stored in the API as snake_case into camelCase. This way the properties in the struct will match the name of the ones in the JSON.

  4. And finally, creates an extension of Animal to expose this mocked data to the rest of the project.

Take a look inside of Preview Content. In there, you’ll find a JSON file, AnimalsMock.json, with a mocked response from the PetFinder’s API. This group is a special group inside the Xcode project that allows you to add mocked code and data to be used inside Xcode Previews. When you later compile the app, Xcode doesn’t include the content of this group in the build. That way, you can store development assets inside the project, like images and other data, and you don’t have to worry about them cluttering your app.

With that, you finished working on the first model of the app’s domain. PetSave is taking shape.

Key points

  • It’s important to plan when designing and architecting your apps.

  • Feature Grouping helps you create highly cohesive and loosely coupled code while still making it easy to identify the scope of each feature.

  • Understanding the app’s domain helps you identify new features and think like the users. Without the domain, you don’t know what you’re building.

  • It’s essential to understand where the domain layer falls in the architecture, its responsibilities and its role.

  • Following design patterns and software principles is great, but it’s essential to understand these principles and where and when to use them.

  • You can create models of your domain and use the Codable protocol to map external objects.

Where to go from here?

If you’re interested in learning more about iOS architectures and MVVM, check out raywenderlich.com’s book Advanced iOS App Architecture.

Also, check out Design Patterns by Tutorials to learn more about design patterns.

Next, you’ll learn about the data layer and how to fetch data from the Petfinder API. Buckle up, because you’re just getting started. :]

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.