OperationQueue — Pass throughSubject — Unit Testing — iOS

Hey Guys!, This is one of my favorite concepts in Swift, and I’m excited to share it with you! In the world of Swift development, managing concurrent tasks, observing changes, and ensuring code reliability are crucial aspects. In this blog post, we’ll dive into three powerful components of Swift that can significantly enhance your development skills and make your code more efficient: OperationQueue, Passthrough Subject, and Unit Testing. By the end of this journey, you’ll have a deeper understanding of how to manage asynchronous operations, communicate between components, and ensure the reliability of your code through comprehensive testing. Let’s get started on this exciting exploration!

lyvennitha sasikumar
11 min readOct 10, 2023

Introduction

Welcome to an exploration of some of Swift’s most potent tools for managing asynchronous operations and ensuring the reliability of your code. In this blog, we will embark on a journey that delves deep into the heart of concurrent programming and Swift’s elegant solutions for it. Our three key players in this narrative are OperationQueue, Passthrough Subject, and Unit Testing.

OperationQueue: As iOS developers, we often find ourselves juggling multiple tasks simultaneously, whether it’s downloading files, processing data, or managing a complex workflow. This is where OperationQueue comes into play. It’s like an orchestral conductor for your asynchronous tasks, ensuring they run smoothly in the background. We’ll not only learn what OperationQueue is but also how to harness its power to execute tasks concurrently, efficiently, and with impeccable organization.

Passthrough Subject: While OperationQueue helps us manage the flow of tasks, Passthrough Subject serves as our communication bridge between different parts of our code. It’s like a messenger that can deliver updates, events, and data changes in real-time. We’ll explore how Passthrough Subject works seamlessly with OperationQueue to notify components of your app about task progress, completion, and more.

Unit Testing: No software journey is complete without testing. We’ll discuss the importance of unit testing and how it ensures the robustness and reliability of your code. By writing unit tests for our asynchronous operations, we can be confident that our code behaves as expected, even as it manages complex tasks asynchronously.

What is operation Queue?

OperationQueue is a powerful mechanism in Swift for managing and executing tasks concurrently. It's built on top of the Operation and OperationQueue classes, which provide a high-level abstraction for managing concurrent operations.

For creating operation queue task, we need operation task and queue to execute.

Operation: An Operation is an abstract class that represents a single unit of work. We can subclass Operation to create our own custom operations, encapsulating tasks we want to perform concurrently.

OperationQueue: An OperationQueue is a queue that manages the execution of Operation objects. It controls the order and concurrency of operations, making it easier to handle tasks concurrently.

Here i’ll show you how to create a queue and operations with simple example and we will explore how to do it in real time scenario.

Create Custom Operations:

  • Subclass Operation to create your own custom operations. Override the main method to define the task you want to perform concurrently within the operation.
import Foundation

class MyCustomOperation: Operation {
override func main() {
// Your task goes here
}
}

Initialise and Configure OperationQueue:

  • Create an instance of OperationQueue to manage your operations.
  • Configure properties like maxConcurrentOperationCount to control the concurrency level.
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 4 // Adjust as needed

Add Operations to the Queue:

  • Create instances of your custom operations and add them to the operation queue.
  • The queue automatically manages the execution of these operations concurrently.
let operation1 = MyCustomOperation()
let operation2 = MyCustomOperation()
operationQueue.addOperation(operation1)//am adding 2 same operation to the queue
operationQueue.addOperation(operation2)
// you can customise with different operation for single queue

TADA!💁🏽‍♀️

But Whats so special about queue and why hype for this!

Wait for Completion (Optional): — we can use this feature if we want

  • If we need to wait for all operations to finish before proceeding, we can use operationQueue.waitUntilAllOperationsAreFinished()
operationQueue.waitUntilAllOperationsAreFinished()

Handle Dependencies (Optional):

  • We can set dependencies between operations to control their execution order. An operation won’t start until all its dependencies are finished.
operation2.addDependency(operation1)

Cancel Operations (Optional):

  • If we need to cancel an operation or a set of operations, we can call cancel() on them.
operation1.cancel()

Pause Operation (Optional):

  • If we need to pause an operation or a set of operations, we can set isSuspended bool to handle pause and resume.
  • For example, in case of connectivity issues, we can prevent re-downloading whole data or creating new request to server, we can use this technique in OperationQueue
 if reachability.connection != .unavailable {
// Network is available, resume the operation queue
operationQueue.isSuspended = false
} else {
// Network is unavailable, pause the operation queue
operationQueue.isSuspended = true
}

Okay this is just intro. Lets delve into real time example🏃🏽‍♀️.

Here I am trying to download image from array of URLs and Want to show the progress. I prefer Operation queue to done it asynchronously.

Firstly I want to create a operation where the URL request created and downloading process of single image.

ImageDownload Operation:

The ImageDownloadOperation class you provided is an Operation subclass designed for downloading images from a given URL. This class incorporates several features to facilitate image downloading, progress reporting, and publishing the local URL of the downloaded image.

Here I wanna introduce one more concept. Will see this later.

class ImageDownloadOperation: Operation, Identifiable {
let imageURL: URL
var image: UIImage?
var progress: Double = 0.0

enum OperationState {
case ready
case executing
case finished
}

private var _state: OperationState = .ready

init(imageURL: URL) {
self.imageURL = imageURL
super.init()
}

override var isAsynchronous: Bool {
return true
}

override var isExecuting: Bool {
return _state == .executing
}

override var isFinished: Bool {
return _state == .finished
}

override func start() {
if isCancelled {
_state = .finished
return
}

_state = .executing
main()
}

override func main() {
if isCancelled { return }

URLSession.shared.dataTask(with: imageURL) { [weak self] (data, response, _) in
guard let self = self else { return }

if let data = data, let img = UIImage(data: data) {
self.image = img
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let localURL = documentsURL.appendingPathComponent(self.imageURL.lastPathComponent)
do {
try data.write(to: localURL)
//self.localURLSubject.send(localURL) // Notify view model
} catch {
print("Error saving image: \(error)")
//self.localURLSubject.send(nil) // Notify view model of error
}
}

// Now Progress Value
let totalBytesExpected = response?.expectedContentLength ?? 0
let totalBytesReceived = Double(data.count )

self.progress = totalBytesReceived / Double(totalBytesExpected)
//self.progressSubject.send(self.progress)
}
self.progress = 1.0
self._state = .finished
}.resume()
}
}

Properties and State:

  • imageURL: A property that stores the URL of the image to be downloaded.
  • image: A property to store the downloaded image.
  • progress: A property to represent the progress of the download, ranging from 0.0 (not started) to 1.0 (completed).

Operation State:

  • OperationState: An enumeration that defines three states for the operation: .ready, .executing, and .finished.
  • _state: A private property that keeps track of the current state of the operation.

Initializer:

  • init(imageURL:): An initializer that accepts the URL of the image to be downloaded and sets the imageURL property.

Overriding Operation Properties and Methods:

  • isAsynchronous: Overrides the property to indicate that the operation is asynchronous.
  • isExecuting: Overrides the property to check whether the operation is executing.
  • isFinished: Overrides the property to check whether the operation is finished.
  • start(): Overrides the start() method to initiate the image download. It checks for cancellation and sets the state to .executing before calling main().

Image Download and Progress Reporting:

  • main(): Overrides the main() method where the actual image download occurs using URLSession's dataTask(with:). It handles the download completion, saving the image to the local file system, and reporting progress updates.

Publishers

The above image download operation, download the image and save it in local. But we have to notify the image status an image in View.

We have multiple ways to get image to viewModel to View. But as a normal case, we prefer completion handlers most and sometimes delegates and observers.

Here am gonna use PassthroughSubject from combine.

Using Combine’s PassthroughSubject (or other publishers) for asynchronous communication and data flow can offer several advantages over using closures

  1. Declarative and Reactive: Combine is built around a declarative and reactive programming model. It allows you to describe what should happen when events occur rather than imperatively defining how to handle those events. This can lead to more concise and expressive code.
  2. Composition: Combine publishers can be easily composed and combined to create complex data flows. You can merge, filter, transform, and combine publishers to handle different scenarios. With closures, handling complex data flows can become unwieldy and hard to maintain.
  3. Error Handling: Combine provides robust error handling mechanisms, allowing you to propagate and handle errors consistently throughout your data flow. Closures, on the other hand, might require more manual error handling and can lead to error-handling boilerplate code.
  4. Cancellable: Combine publishers can be easily canceled or retained as cancellable objects. This is particularly useful for managing resource cleanup and avoiding strong reference cycles. Closures do not offer this level of resource management out of the box.
  5. Integration with SwiftUI: If you’re working with SwiftUI, Combine and publishers are a natural fit. Many SwiftUI components, such as @Published properties and ObservableObject, work seamlessly with Combine publishers.
  6. Extensibility: Combine is a framework, which means it can be extended with custom publishers and operators. You can create custom publishers to encapsulate complex logic and reuse it throughout your codebase.

Now, i declare two passthrough subject variables. One for image and another for progress.

private var localURLSubject = PassthroughSubject<URL?, Never>()
var localURLPublisher: AnyPublisher<URL?, Never> {
return localURLSubject.eraseToAnyPublisher()
}

private var progressSubject = PassthroughSubject<Double, Never>()
var progressPublisher: AnyPublisher<Double, Never>{
return progressSubject.eraseToAnyPublisher()
}

Declare this in ImageDownloadOperation class.

Now assign image once downloaded.

do {
try data.write(to: localURL)
self.localURLSubject.send(localURL) // Notify view model
} catch {
print("Error saving image: \(error)")
self.localURLSubject.send(nil) // Notify view model of error
}

Also same for progress data

let totalBytesExpected = response?.expectedContentLength ?? 0
let totalBytesReceived = Double(data.count )

self.progress = totalBytesReceived / Double(totalBytesExpected)
self.progressSubject.send(self.progress)

Now we gonna create viewModel.

The ImageDownloadViewModel class , a SwiftUI ViewModel responsible for managing a collection of ImageDownloadOperation instances, downloading images, and updating the UI with download progress and local image URLs.


class ImageDownloadViewModel: ObservableObject {
@Published var imageTasks: [ImageDownloadOperation] = []
@Published var localImageURLs: [URL?] = []
@Published var downloadProgress: [Double] = []

private var cancellables: Set<AnyCancellable> = []

init(imageURLs: [URL]) {
for item in imageURLs{
// self.imageTasks.append(ImageDownloadOperation(imageURL: item))
let operation = ImageDownloadOperation(imageURL: item)
imageTasks.append(operation)

// Subscribe to the localURLPublisher of each operation
operation.localURLPublisher
.sink { [weak self] localURL in
guard let self = self else { return }
DispatchQueue.main.async {
if let index = self.imageTasks.firstIndex(of: operation) {
self.localImageURLs[index] = localURL
}
}
}
.store(in: &cancellables)
// Subscribe to the progressPublisher of each operation
operation.progressPublisher
.sink { [weak self] progress in
guard let self = self else { return }
DispatchQueue.main.async {
if let index = self.imageTasks.firstIndex(of: operation) {
self.downloadProgress[index] = progress
}
}
}
.store(in: &cancellables)
}
downloadProgress = Array(repeating: 0.0, count: imageTasks.count)
localImageURLs = Array(repeating: nil, count: imageURLs.count)

}

func downloadAction(){
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 7
for item in imageTasks{
operationQueue.addOperation(item)
}

}
}

Properties:

  • imageTasks: An array of ImageDownloadOperation instances representing the image download operations.
  • localImageURLs: An array of optional URL values representing the local file URLs where downloaded images are stored.
  • downloadProgress: An array of Double values representing the progress of image downloads.
  • cancellables: A set of AnyCancellable objects to manage Combine subscriptions.

Initializer (init(imageURLs:)):

  • The initializer takes an array of URL objects representing the URLs of images to be downloaded.
  • It creates an ImageDownloadOperation for each URL and adds them to the imageTasks array.
  • For each operation, it subscribes to its localURLPublisher and progressPublisher to receive updates about the local URL and download progress.
  • It initializes the downloadProgress and localImageURLs arrays with default values, setting the progress to 0.0 and local URLs to nil for each image.

downloadAction():

  • This method initiates the image download by adding the ImageDownloadOperation instances to an OperationQueue. The queue is configured with a maximum concurrent operation count of 7, meaning up to seven download operations can run simultaneously.
  • You can uncomment the operationQueue.isSuspended = true line to initially pause the operation queue if you want to control when the downloads start.

Combine Subscriptions:

  • Inside the initializer, subscriptions to localURLPublisher and progressPublisher of each ImageDownloadOperation are set up. When a download operation completes and emits updates, the view model updates the localImageURLs and downloadProgress arrays accordingly.

DispatchQueue for UI Updates:

  • Since Combine publishers emit events on a background queue by default, the view model uses DispatchQueue.main.async to update the UI elements, ensuring that UI updates happen on the main thread.

Now its time for ContentView(UI):

The ContentView struct, a SwiftUI view that displays a list of images along with progress bars, representing the download progress of those images. This view relies on the ImageDownloadViewModel to manage the image download operations and track their progress.


struct ContentView: View {

@StateObject var viewModel = ImageDownloadViewModel(imageURLs: [URL(string: "https://picsum.photos/1000")!, URL(string: "https://picsum.photos/3000")!, URL(string: "https://picsum.photos/2000")! ])

var body: some View {
List(0..<viewModel.localImageURLs.count, id: \.self) { index in
if let localURL = viewModel.localImageURLs[index] {
HStack{
Image(uiImage: (UIImage(contentsOfFile: localURL.path) ?? UIImage(systemName: "square.and.arrow.up"))!)
.resizable()
.frame(width: 50, height: 50)
ProgressView(value: viewModel.downloadProgress[index]) // Display progress bar
.frame(width: 250, height: 25)
}
} else {
ProgressView(value: viewModel.downloadProgress[index]) // Display progress bar
.frame(width: 250, height: 25)
}
}
.onAppear(){
viewModel.downloadAction()
}
}
}

@StateObject and View Model Initialization:

  • The @StateObject property wrapper is used to create an instance of ImageDownloadViewModel as part of the view's state. This ensures that the view model is retained and available as long as the view is alive.
  • The view model is initialized with an array of URL objects, which represent the URLs of the images to be downloaded.

List View:

  • The main content of the view is a SwiftUI List that displays a dynamic list of items.
  • The list is created with a range of indices, from 0 to the count of viewModel.localImageURLs, to generate a list item for each downloaded image.
  • Each list item corresponds to an image at a specific index.

Image Display and Progress Bar:

  • Within each list item, there’s a conditional check to determine whether a local URL exists for the image at the current index (viewModel.localImageURLs[index]).
  • If a local URL exists, an Image view is created to display the downloaded image. If the local URL doesn't exist, a placeholder image (system image "square.and.arrow.up") is used.
  • A ProgressView is also displayed, showing the download progress. This progress bar's value is determined by viewModel.downloadProgress[index].

onAppear Event:

  • When the view appears on the screen (onAppear event), the viewModel.downloadAction() method is called. This initiates the download of the images.

Dynamic Updates:

  • As the image download operations progress and complete, the view model updates the localImageURLs and downloadProgress arrays accordingly.
  • SwiftUI’s data binding and reactive nature ensure that any changes in these arrays trigger updates to the view, causing it to re-render and reflect the updated image and progress information.

Now its time for unit testing:

These unit tests are designed to verify the functionality of the ImageDownloadOperation and ImageDownloadViewModel classes in our code.

  func testImageDownloadOperationInitialization() {
let imageURL = URL(string: "https://www.example.com/image.jpg")!
let operation = ImageDownloadOperation(imageURL: imageURL)
XCTAssertEqual(operation.imageURL, imageURL)
}

func testImageDownloadViewModelInitialization() {
let imageURLs = [URL(string: "https://www.example.com/image1.jpg")!, URL(string: "https://www.example.com/image2.jpg")!]
let viewModel = ImageDownloadViewModel(imageURLs: imageURLs)
XCTAssertEqual(viewModel.imageTasks.count, 2)
XCTAssertEqual(viewModel.localImageURLs.count, 2)
}

func testImageDownloadViewModelDownloadAction() {
let imageURL = URL(string: "https://www.example.com/image.jpg")!
let viewModel = ImageDownloadViewModel(imageURLs: [imageURL])

let expectation = XCTestExpectation(description: "Image download completed")


viewModel.downloadAction()

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// Assert that the download action has completed successfully
XCTAssertNotNil(viewModel.localImageURLs.first)

expectation.fulfill()
}

wait(for: [expectation], timeout: 5)
}

testImageDownloadOperationInitialization()

  • This test checks the initialization of an ImageDownloadOperation instance.
  • It creates an imageURL representing a URL of an image and then creates an ImageDownloadOperation with that URL.
  • Finally, it uses XCTAssertEqual to verify that the imageURL property of the operation matches the URL provided during initialization.

testImageDownloadViewModelInitialization()

  • This test verifies the initialization of an ImageDownloadViewModel instance.
  • It creates an array of imageURLs, representing URLs of images, and then initializes a ImageDownloadViewModel with those URLs.
  • The test checks two aspects:
  • It ensures that the imageTasks array in the view model contains the expected number of ImageDownloadOperation instances, which is 2 in this case.
  • It also checks that the localImageURLs array in the view model contains the expected number of elements, which is again 2.

testImageDownloadViewModelDownloadAction()

  • This test validates the downloadAction() method of the ImageDownloadViewModel.
  • It starts by creating a ImageDownloadViewModel instance with an array containing one imageURL.
  • Then, it creates an expectation using XCTestExpectation to wait for the image download to complete.
  • The test invokes the downloadAction() method, which initiates the download of the image.
  • It uses DispatchQueue.main.asyncAfter to introduce a delay (3 seconds) to allow time for the download to complete.
  • Within the closure passed to asyncAfter, it asserts that the localImageURLs array in the view model has a non-nil value for the first image.
  • Finally, the test fulfills the expectation to signal that the test has completed.

Follow me for more exciting concepts in iOS

Thank you!

Happy Coding!🧚🏼‍♀️

--

--

lyvennitha sasikumar
lyvennitha sasikumar

Written by lyvennitha sasikumar

Passionate iOS developer chasing my dreams toward success

Responses (1)