Swift Combine (Part 3 of 3) — Building Real-Case App

Fernando Putra
9 min readDec 24, 2023

--

Welcome to the last part of my Swift Combine series. In this chapter, you’ll integrate the network request into a real-case SwiftUI application. Covering various topics sequentially and interdependently, from the app’s architecture to testing strategies.

The Objective

Continuing from the network request function developed in the previous chapter, we’ll now construct a SwiftUI application and integrate it. Quickly remind, the application we’re building will enable users to search for posts from the JSONPlaceholder API based on the inputted title, dynamically presenting the results on the screen. The wireframe is illustrated below:

App’s Wireframe

We’ll go through six sections in order, providing brief explanations for each as well as the code snippets and their explanations.

Architecting with MVVM

When laying the foundation for the app, the architectural decisions significantly impact the project’s scalability, maintainability, and testability. In this chapter, we’ll use the Model-View-ViewModel (MVVM) architecture. While it’s important to note that any architectural patterns are viable with Combine Framework, we opted for MVVM due to its popularity among developers and the compelling reasons it offers, including:

  1. Separation of Concerns: MVVM cleanly separates data, presentation logic, and UI concerns for improved code organization.
  2. Reactive UI Updates: SwiftUI’s reactivity, combined with MVVM and Combine, ensures real-time updates for a responsive user interface.
  3. Testability: MVVM’s encapsulation of business logic in the ViewModel enhances unit testability, aided by Combine’s testing features.
  4. Maintainability and Scalability: MVVM’s modular structure facilitates independent development and scaling without disrupting the entire codebase.

Create a new iOS App Project and implement the following snippet code:

With this code, you:

  1. Create a Models folder and place three Swift files inside it, each corresponding to a data model: User, Post, and PostDetail. These files will define the underlying structure of your app.
  2. Create a ViewModels folder and place a ViewModel class named SearchPostsViewModel conforming to the ObservableObject protocol. This class will manage the app’s data and business logic.
  3. Inside the SearchPostsViewModel class, use a @Published property wrapper to declare the queryInput property. You might wonder why we didn’t use CurrentValueSubject, as in the previous chapter. It’s because both are quite similar as they can hold a value and subscribe; I see @Published as syntactic sugar for CurrentValueSubject.
  4. Create a Views folder and place a SwiftUI view named SearchPostsView. Declare a SearchPostsViewModel using the @StateObject property wrapper. Alternatively, you can use @ObservedObject if you intend to share an existing instance. The key distinction lies in lifecycle management: @StateObject creates a new instance for the view, while @ObservedObject uses an existing one, potentially shared with other views.

Handling The App State

Handling the app state involves managing and responding to the application’s state, ensuring that the user interface reflects the current conditions and interactions within the app. This includes managing data loading, user input, navigation, and responding to various events that impact the application’s behavior. In SwiftUI, you can use property wrappers such as @State, @ObservedObject, and @StateObject to handle the states. For example, you might use a loading boolean to toggle between the loading and success states. Here’s an elaboration on various states, including initial, loading, success, and errors:

  1. Initial State: Represents the starting condition of the app, typically encountered upon launch or when loading a specific screen.
  2. Loading State: Indicates that the app is actively fetching or processing data, and visual indicators are often used to inform users of ongoing background operations.
  3. Success State: Signifies the successful completion of a task or operation, such as retrieving data from a server and prompting updates to the UI to display the fetched information.
  4. Error State: Occurs when an unexpected issue, such as a network error or failed data retrieval, is encountered. It prompts communication of the error to the user with relevant information and potential resolution actions.

Continue and implement the following snippet code:

With this code, you:

  1. Create an Enums folder and place a new Swift file named AppStateEnum. Define a generic enumeration named AppState to represent the various states of your app. The generic type T allows flexibility in the data associated with certain cases.
  2. Inside the SearchPostsViewModel class, define a state property with the type AppState<[PostDetail]>. It’s initialized with the .initial case, representing the initial state of the view with associated data of type [PostDetail].

Injecting The Service Layer

SwiftUI, as a declarative UI framework, encourages the adoption of clear and modular architecture, making dependency injection a popular choice for structuring projects. Dependency injection (DI) is a design pattern that involves providing a component with its dependencies rather than letting the component create or find them. This pattern enhances testability, maintainability, and flexibility by decoupling components and explicitly specifying their dependencies.

In this context, dependency injection provides the ViewModel with its dependencies, such as the service layer responsible for making API calls. These dependencies are typically injected through initializers.

Continue and implement the following snippet code:

With this code, you:

  1. Create a Services folder and place a new Swift file named JSONPlaceholderAPIService. Define a JSONPlaceholderAPIServiceProtocol and methods for fetchUsers() and fetchPosts() that returns a Combine AnyPublisher. Using protocols provides a clear and abstract interface for your services, allowing you to define the expected behaviour without specifying the implementation details
  2. Create a JSONPlaceholderAPIService class conforming to the JSONPlaceholderAPIServiceProtocol. This is the concrete implementation of the service layer.
  3. Inside the JSONPlaceholderAPIService class, define constants for the API URLs to make them more readable and maintainable.
  4. Declares a session private property with the type of URLSession and initializes inside the init method of the JSONPlaceholderAPIService class.
  5. Implement the fetchUsers() and fetchPosts() methods, along with the fetchData() method that encapsulates the generic process of the networking and decoding logic, promoting code reusability across different endpoints.
  6. Inject theJSONPlaceholderAPIService layer inside the SearchPostsViewModel class through the ViewModel’s initializer. This allows different implementations of the service layer, making it easy to switch between a production service and a mock service for testing.
  7. Provide an instance of JSONPlaceholderAPIService to the SearchPostsViewModel initializer. This is where the dependency injection is in action.

Integrating The Network Requests

It’s time to integrate the network requests we developed in the previous chapter into our app. Keep in mind that there are some adjustments to our network request functions as we handle app states and leverage the SwiftUI property wrapper, particularly @Published. However, the core logic of the functions remains unchanged.

Continue and implement the following snippet code:

With this code, you:

  1. Declare a set of AnyCancellable, which will be used to manage the subscriptions.
  2. Inside the init method, call the setupBindings() method to establish Combine bindings. This method sets up a pipeline that reacts to changes in the queryInput.
  3. If the query is not empty, the fetchPosts(with:) method is called to initiate the network request. Otherwise, the state is set to .initial.
  4. Inside the fetchPosts(with:) method, sets the state to .loading to indicate that a network request is in progress.
  5. Handle the completion events by updating the state to .error if an error occurs, and to .success if the request is successful.

Creating The UI

With the backend in place, we can now focus on crafting the SearchPostsView. The view adeptly should respond to changes in the associated SearchPostsViewModel’s state, dynamically presenting distinct views depending on the state condition, such as prompting the user’s input in the initial state, indicating loading with a ProgressView, displaying search results, or handling errors. Then, integrate the search functionality using the .searchable modifier bound to the queryInput property.

Continue and implement the following snippet code:

With this code, you:

  1. Display different views based on the state of the associated SearchPostsViewModel.
  2. In the .initial state, display a stateView function prompting the user to enter a title to find matching posts.
  3. In the .loading state, display a ProgressView, indicating that the app is fetching data.
  4. In the .success state, use the postsList function to display posts if available; otherwise, display a stateView function suggesting the user try a different title.
  5. In the .error state, display a stateView function indicates that something went wrong and advises trying again later.
  6. Apply the .searchable modifier to enable search functionality using the queryInput property from the viewModel.

Here is the app demo of what we’ve developed:

App Demo

Writing The Unit Tests

Unit testing is a crucial practice in software development that involves examining individual code units to ensure their correctness and reliability. When it comes to SwiftUI applications, unit tests are essential for verifying the behaviour of ViewModel components. By isolating and evaluating these components, we can identify potential bugs early in the development process, streamline debugging efforts, and enhance the overall robustness of our application.

This section will focus on testing the SearchPostsViewModel to guarantee that it effectively manages search functionality, network requests, and state transitions. We start by creating a unit testing bundle to initiate the testing process. Here are the steps:

  1. In the Xcode toolbar, click on File > New > Target… to open the template chooser.
  2. In the template chooser, under the Test category, select Unit Testing Bundle.
  3. Click Next to proceed.
  4. Give your testing bundle a name. Ensure the Targets checkbox is selected for your main app target.
  5. Click Finish to create the testing bundle.

Now that we’ve established a dedicated folder for our tests, the next step is creating a mock of the. JSONPlaceholderAPIService to control and manipulate responses during the testing process.

Create a new Swift file named MockJSONPlaceholderAPIService and add the following code:

With this code, you:

  1. Import the necessary modules and classes for testing, specifically importing the main project module Building_Real_Case_App.
  2. Create a MockJSONPlaceholderAPIService class that subclasses JSONPlaceholderAPIService to simulate fetching users and posts with predefined data. This mock service is designed for testing purposes and allows controlled responses.
  3. Create a MockErrorJSONPlaceholderAPIService class that also subclasses JSONPlaceholderAPIService but simulates an error scenario by returning a failure with a URLError. This mock service is useful for testing error handling in the application.

Then, we create a unit testing class named SearchPostsViewModelTests. Continue and implement the following code snippet.

Note: I only provide testFetchPostsSuccess() test in the snippet to minimize a bunch of code; You can always see the full code on the repository.

  1. Initialize the MockJSONPlaceholderAPIService, the SearchPostsViewModel (sut), and a set of cancellables to handle Combine cancellations.
  2. The setUp function is overridden to instantiate and set up the initial state for the service, the viewModel, and cancellables.
  3. The tearDown function is overridden to clean up and release resources after each test, setting the service, viewModel, and cancellables to nil.
  4. The testFetchPostsSuccess function is a unit test focusing on the success scenario when fetching posts.
  5. Set up a sampleQuery, expectedResult for fetching posts, and expectedStates
  6. Create an expectation to await the fulfilment of asynchronous tasks during the test.
  7. Observe the viewModel’s state changes, and the corresponding actions are taken in response to the state changes.
  8. Add an assertion to check whether the received states match the expectedStates.
  9. In the success state, add an assertion to check whether the fetched posts match the expectedResult.
  10. After ensuring that the expectation defined in point 6 is fulfilled, call expectation.fulfill() to signal that the asynchronous task (state transition) has been completed, allowing the test to proceed to the next steps or conclude successfully.
  11. Set the queryInput to trigger the fetching of posts, and the test waits for the expectation to be fulfilled.

Lastly, to check the code coverage for the SearchPostsViewModel, you can open the code coverage report to see how much of the code is covered by your tests. Follow the steps below:

  1. After the tests are run, open the Report Navigator by clicking on the square icon on the left side of the Xcode window.
  2. Find and click on the latest test report.
  3. In the test report, you should see a Coverage tab. Click on it to open the code coverage report.
Code Coverage Report

Congrats! You’ve completed my Swift Combine series. I hope this series helped you gain the knowledge and skills needed to excel in Swift development. Remember, the journey doesn’t end here; continue exploring, experimenting, and refining your coding expertise. Happy coding!

If you found this article helpful, kindly share it with your friends and follow my profile for future updates. Your support is much appreciated!

You can access the repository for this series below:

PS: I’m always welcome for pizza 🍕🍕🍕.

--

--