Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Mastering iOS 18 Development

You're reading from   Mastering iOS 18 Development Take your iOS development experience to the next level with iOS, Xcode, Swift, and SwiftUI

Arrow left icon
Product type Paperback
Published in Nov 2024
Publisher Packt
ISBN-13 9781835468104
Length 418 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Avi Tsadok Avi Tsadok
Author Profile Icon Avi Tsadok
Avi Tsadok
Arrow right icon
View More author details
Toc

Table of Contents (20) Chapters Close

Preface 1. Part 1: Getting Started with iOS 18 Development
2. Chapter 1: What’s New in iOS 18 FREE CHAPTER 3. Chapter 2: Simplifying Our Entities with SwiftData 4. Chapter 3: Understanding SwiftUI Observation 5. Chapter 4: Advanced Navigation with SwiftUI 6. Chapter 5: Enhancing iOS Applications with WidgetKit 7. Chapter 6: SwiftUI Animations and SF Symbols 8. Chapter 7: Improving Feature Exploration with TipKit 9. Chapter 8: Connecting and Fetching Data from the Network 10. Chapter 9: Creating Dynamic Graphs with Swift Charts 11. Part 2: Refine your iOS Development with Advanced Techniques
12. Chapter 10: Swift Macros 13. Chapter 11: Creating Pipelines with Combine 14. Chapter 12: Being Smart with Apple Intelligence and ML 15. Chapter 13: Exposing Your App to Siri with App Intents 16. Chapter 14: Improving the App Quality with Swift Testing 17. Chapter 15: Exploring Architectures for iOS 18. Index 19. Other Books You May Enjoy

Exploring Networking and Combine

Networking is a great place to start if you haven’t worked with Combine. Combine is a framework that declaratively handles a stream of values over time while supporting asynchronous operations.

Based on that description, it looks like Combine was made for networking operations!

In this chapter, we are not going to discuss what Combine is – for that, we’ve got Chapter 11. However, we are going to discuss it now because Combine is a great way to solve many networking operations problems.

Since Combine is built upon publishers and operators, it is simple to create new publishers that stream data from the network.

Let’s try to request the list of contacts from previous examples using a Combine stream. We’ll start with creating a publisher that performs data fetching from the network and publish a list of contacts:

class ContactRequest {
    func fetchData() -> AnyPublisher<[Contact], Error> {
        let url = URL(string:
          "https://api.example.com/contacts")!
        return URLSession.shared.dataTaskPublisher(for:
          url)
            .map { $0.data }
            .decode(type: [Contact].self, decoder:
              JSONDecoder())
            .eraseToAnyPublisher()
    }
}

The publisher utilizes URLSession’s dataTaskPublisher method to execute the network request and publish the retrieved data. We then extract the data using the map operation and decode it into a list of Contact items. If something goes wrong, the publisher will report an Error. We wrap this function in a class named ContactRequest to maintain separation.

Now, let’s create a small DataStore class so we can store the results and publish them:

class DataStore {
    @Published var contacts: [Contact] = []
}

The @Published property wrapper creates a publisher for contacts so that we can observe the changes easily.

Now, we can use the fetchData() function to read the results and store them:

class ContactsSync {
    let contactRequest = ContactRequest()
    let dataStore = DataStore()
    func syncContacts() {
        contactRequest.fetchData()
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("Data fetch completed
                      successfully")
                case .failure(let error):
                    print("Error fetching data: \(error)")
                }
            }, receiveValue: { [weak self] contacts in
                self?.dataStore.contacts = contacts
            })
            .store(in: &cancellables)
    }
    private var cancellables = Set<AnyCancellable>()
}
let contactsSync = ContactsSync()
contactsSync.syncContacts()

The ContactsSync job is to fetch contacts using the ContactRequest class and to store them in the data store using the DataStore class.

The Combine example has several advantages:

  • Clear and consistent interface: The publisher interface is consistent and known. It is always built from data/void and an optional error. New developers don’t need to learn and understand how to read/use it.
  • Built-in error handling: Not only do we have a consistent interface that also contains errors, but also, when one of the stages encounters an error, it interrupts the flow and channels it downstream. We have already seen that error handling is a critical topic in networking in many cases.
  • Asynchronous operations support: We often think that a network operation contains one asynchronous operation: the request itself. However, many steps in the stream can be asynchronous – including preparing the request by reading local data, processing the response, and storing the data at the end of the stream. Combine streams are perfect for performing all those steps asynchronously.
  • Modularity: The capability of building a modular code is reserved not only for the Combine framework, but the custom publishers and the different operators make Combine streams a joyful framework to implement when dealing with networking. Remember that we said that networking is like a production line (under the Understanding mobile networking section)? So, Combine makes it easier to insert more steps into the stream; some of them are even built into the framework.

Adding reactive methods to our code doesn’t mean we need to discard all the design patterns and principles we discussed when we covered networking—it’s just another way to implement them.

For example, let’s try to implement the delta updates design pattern using the Combine framework:

        URLSession.shared.dataTaskPublisher(for: request)
            .tryMap { output in
                guard let response = output.response as?
                  HTTPURLResponse, response.statusCode ==
                  200 else {
                    throw URLError(.badServerResponse)
                  }
                return output.data
            }
            .decode(type: ContactsDeltaUpdateResponse.self,
              decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    print("Error during sync:
                      \(error.localizedDescription)")
                }
            }, receiveValue: { [weak self] response in
                self?.processDeltaUpdates(response:
                  response)
            })
            .store(in: &cancellables)

Looking at the code example, we can see that it looks pretty much like the previous Combine code—that’s part of the idea of consistent interface and modular code. We perform the request, check the response code, decode it, change it to the main thread, and process the response data.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image