SwiftUI – Combine

By | 07/06/2023

In this post, we will see what Combine is and how we can use it in our projects.
But first of all, what is Combine?
Combine is a powerful reactive programming framework introduced by Apple that simplifies the handling of asynchronous events and data flow in applications. By leveraging Combine with SwiftUI, we can create more efficient and responsive applications using a declarative approach, making it easier to reason about the code and manage complex data interactions.
With Combine, we can create publishers and subscribers to propagate data changes and updates across your app. Publishers emit values, while subscribers listen for these values and perform tasks based on the received data. This pattern enables us to establish a reactive and data-driven architecture in your SwiftUI apps.
For all information we can look up on the Apple website.
Let’s see some examples of how to use Combine.

CALL A REST API SERVICE
In this example, we will use Combine to call JSONPlaceholder for getting a list of Post objects and show the list in the UI. We remember that, JSONPlaceholder, is a free online REST API service to generate fake JSON data for testing and prototyping purposes.

We start defining the Model file for the entity Post:
[POST.SWIFT]

struct Post: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}


Then, we define its ViewModel:
[POSTVIEWMODEL.SWIFT]

import Combine
import Foundation

class PostViewModel: ObservableObject {
    // The @Published property wrapper creates a publisher for the property, allowing other
    // components to react to changes in its value.
    @Published var posts: [Post] = []
    // This line declares a private property named cancellables that will store a
    // set of AnyCancellable objects.
    // These objects represent active subscriptions to Combine publishers.
    // Storing them in a set ensures that they are not deallocated while they are still active.
    private var cancellables = Set<AnyCancellable>()

    func fetchPosts() {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
        // This line creates a publisher that initiates a URL session data task for the specified URL.
        // The publisher emits the data and URL response upon successful completion of the request.
        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            // The sink operator is used to subscribe to the publisher. It takes two closures as parameters:
            // one for handling the completion event and another for handling the received values
            .sink { completion in
                switch completion {
                case .failure(let error):
                    print("Error fetching posts: \(error.localizedDescription)")
                case .finished:
                    print("Fetching posts completed")
                }
            } receiveValue: { posts in
                self.posts = posts
            }
            // This line adds the created subscription to the cancellables set to prevent it
            // from being deallocated before completion.
            .store(in: &cancellables)
    }
}


Finally, in the ContentView file, we define the View for showing the list of Posts:
[CONTENTVIEW.SWIFT]

import SwiftUI

struct ContentView: View {
    @StateObject private var postViewModel = PostViewModel()
    var body: some View {
        NavigationStack {
            List(postViewModel.posts) { post in
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
            .navigationTitle("Posts")
            .onAppear {
                postViewModel.fetchPosts()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


We have done and now, if we run the application, the following will be the result:


REAL-TIME VALIDATION
In this example, we will see how to use Combine for real-time validation and input handling of a SwiftUI form with three fields (name, email and age).

We start defining the Model file for the entity Form:
[FORM.SWIFT]

struct FormModel {
    var name: String = ""
    var email: String = ""
    var age: String = ""
}


Then we define its ViewModel:
[FORMVIEWMODEL.SWIFT]

import Combine
import Foundation

class FormViewModel: ObservableObject {
    @Published var form = FormModel()

    // The validation state for each form field.
    @Published var isNameValid = false
    @Published var isEmailValid = false
    @Published var isAgeValid = false

    init() {
        // Validate the 'name' field when its value changes.
        $form
            // Extract the 'name' value from the form model.
            .map(\.name)
            // Limit the frequency of validation checks to once every 0.2 seconds.
            .debounce(for: 0.2, scheduler: DispatchQueue.main)
            // Check if the name has at least 3 characters.
            .map { $0.count >= 3 }
            // Update the 'isNameValid' property with the validation result.
            .assign(to: &$isNameValid)

        $form
            .map(\.email)
            .debounce(for: 0.2, scheduler: DispatchQueue.main)
            // Check if the email is valid using the 'isValidEmail' function.
            .map { $0.isValidEmail() }
            .assign(to: &$isEmailValid)

        $form
            .map(\.age)
            .debounce(for: 0.2, scheduler: DispatchQueue.main)
            // Check if the age is a positive integer.
            .map { Int($0) != nil && Int($0)! > 0 }
            .assign(to: &$isAgeValid)
    }
}

// String extension to add an 'isValidEmail' function for email validation.
extension String {
    func isValidEmail() -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        return emailPredicate.evaluate(with: self)
    }
}


Finally, in the ContentView file, we define the View for showing the Form:
[CONTENTVIEW.SWIFT]

import SwiftUI

struct ContentView: View {
    @StateObject private var formViewModel = FormViewModel()

        var body: some View {
            NavigationStack {
                Form {
                    Section(header: Text("Personal Information")) {
                        TextField("Name", text: $formViewModel.form.name)
                        TextField("Email", text: $formViewModel.form.email)
                        TextField("Age", text: $formViewModel.form.age)
                    }

                    Button(action: submitForm) {
                        Text("Submit")
                    }
                    .disabled(!formViewModel.isNameValid || !formViewModel.isEmailValid || !formViewModel.isAgeValid)
                }
                .navigationTitle("Form")
            }
        }

        func submitForm() {
            print("Name: \(formViewModel.form.name)")
            print("Email: \(formViewModel.form.email)")
            print("Age: \(formViewModel.form.age)")
        }
    }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


We have done and now, if we run the application, the following will be the result:



Leave a Reply

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