Skip to main content

10 questions tagged with "Concurrency"

Concurrency tag description

View All Tags

Can you explain the different quality of service options GCD provides?

· 4 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources
TL/DR

The different Quality of Service (QoS) options in Grand Central Dispatch (GCD) are used to prioritize tasks based on their importance and urgency:

  • .userInteractive: Highest priority for tasks interacting with the user, like UI updates. These tasks should be completed immediately to avoid freezing the app.
  • .userInitiated: Used for tasks initiated by the user that need to be completed quickly, such as fetching data or opening files. Typically completed within seconds.
  • .utility: For longer-running tasks that don’t need immediate results, such as downloading files. Provides a balance between responsiveness and efficiency, usually completed in seconds to minutes.
  • .background: Lowest priority for tasks that are not visible to the user, like maintenance work. These tasks can take from minutes to hours to complete.

Assigning the correct QoS helps optimize app performance and energy efficiency.

The quality-of-service (QoS) options available on DispatchQueue allow us to categorize the importance of the work we’re scheduling. The system will then intelligently prioritize tasks with higher quality-of-service designations.

Since higher priority work is performed more quickly and with more computational resources than lower priority work, it typically requires more energy than lower priority work. So, by accurately specifying appropriate QoS classes for the work your app performs, you can help ensure that your app is responsive and energy efficient.

There are 4 QoS options we’ll look at in decreasing order of priority and performance:

.userInteractive

This designation should be used for work that is interacting with the user (i.e. refreshing the user interface or performing animations). In other words, it should be used for work that is of such high importance that if it doesn’t happen quickly the application may appear frozen.

Any work queued with this QoS designation happens nearly instantaneously.

.userInitiated

This is used for work that the user has initiated and requires immediate results. Actions like retrieving information from an API, opening or modifying a file, or generally any work that needs to be completed in order to continue with the user flow.

Any work queued with this QoS designation is generally completed within a few seconds or less.

.utility

This is used for work that may take some time to complete and doesn’t require an immediate result - actions like downloading or importing data.

Generally, tasks with this designation will also show some type of progress indicator to the user (like you see when you download a podcast or a Netflix episode). This QoS provides a balance between responsiveness, performance, and energy efficiency.

Any work queued with this QoS designation is generally completed within a few seconds to a few minutes.

.background

This final designation has the lowest priority and is used for tasks that are not visible to the user like indexing, synchronizing, backups, saving data, or any general purpose maintenance work.

Any work queued with this QoS designation can take considerable time to complete; anywhere from a few minutes to a few hours as the computational resources and priority given to tasks with this designation are minimal.

By categorizing the tasks you send to DispatchQueue, you enable the system to optimize the completion of those tasks by reallocating resources away from lower priority work and redirecting it to the higher priority tasks instead.

In Bullets
  • .userInteractive:

    • Highest priority.
    • For tasks interacting directly with the user (e.g., UI updates, animations).
    • Must be completed immediately to keep the app responsive.
  • .userInitiated:

    • High priority.
    • For tasks initiated by the user requiring quick results (e.g., fetching data, opening files).
    • Typically completed within seconds.
  • .utility:

    • Medium priority.
    • For longer-running tasks not requiring immediate results (e.g., downloading files).
    • Balances responsiveness and efficiency; completed in seconds to minutes.
  • .background:

    • Lowest priority.
    • For non-visible tasks like maintenance work (e.g., backups, indexing).
    • Can take from minutes to hours to complete.

How can we group multiple asynchronous tasks together?

· 3 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources
TL/DR

Swift’s DispatchGroup and the newer async/await syntax both allow us to group and monitor the completion of multiple asynchronous tasks. DispatchGroup offers a traditional approach, while async/await provides a more modern, readable way to handle asynchronous operations.

In Swift, grouping multiple asynchronous tasks can be achieved using either DispatchGroup or async/await.

Using DispatchGroup​

DispatchGroup is a traditional tool used to group multiple asynchronous tasks and monitor their completion. It enables you to schedule tasks on the same or different queues and ensures that all tasks are completed before proceeding with further actions, such as notifying the user.

Example: Uploading Images with DispatchGroup​

class DispatchGroupDemo {
func uploadImages(images: [UIImage]) {
let group = DispatchGroup()
for image in images {
group.enter()
ImageUploader.upload(image) {
// Successfully uploaded photo
group.leave()
}
}
group.notify(queue: .main) {
// Show user success message
}
}
}

In this example, each image is uploaded independently, and only after all uploads are complete does the notify closure execute.

Using async and await​

The async/await syntax in Swift offers a more modern approach, allowing asynchronous code to be written in a more linear and readable manner.

Example: Uploading Images with async/await​

class AsyncAwaitDemo {
func uploadImages(images: [UIImage]) async {
for image in images {
await ImageUploader.upload(image)
}
// Show user success message after all uploads are complete
}
}

class ImageUploader {
static func upload(_ image: UIImage) async {
// Simulate image upload with async/await
try? await Task.sleep(nanoseconds: 1_000_000_000) // Simulates a 1-second upload delay
}
}

This example simplifies the code, making it easier to follow by using the await keyword to pause execution until each asynchronous task completes.

Use Cases​

  • Independent Web Requests: When multiple pieces of data need to be fetched asynchronously before proceeding.
  • Batch Processing: Such as uploading a collection of photos independently, where completion of all uploads triggers a final action.

Alternative: OperationQueue​

If you prefer using OperationQueue, it allows you to add dependencies between tasks, ensuring they complete sequentially while treating them as a related group.

In Bullets
  • DispatchGroup Usage: Group and synchronize multiple asynchronous tasks.
  • Async/Await: Modern, more readable approach for handling asynchronous code.
  • Independent Execution: Each task runs independently but is monitored collectively.
  • OperationQueue Alternative: Allows for sequential task dependencies.

How can we prevent race conditions [the reader-writer problem]?

· 6 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources
TL/DR

Prevent race conditions by ensuring synchronized access to shared resources using mechanisms such as serial queues, DispatchBarrier, NSLock, @synchronized, higher-level constructs like OperationQueue, Readers-Writers Locks, and Swift’s async/await model.

Race Conditions in Concurrent Programming​

Race conditions occur when multiple threads or tasks attempt to access and modify shared resources simultaneously, leading to unpredictable and erroneous behavior. To prevent this, synchronization mechanisms are employed to control the access and update of shared resources in a predictable manner.

Solutions to Prevent Race Conditions​

There are several methods to prevent race conditions in Swift, particularly when dealing with the reader-writer problem:

1. Serial Dispatch Queues​

Serial queues ensure that only one task runs at a time, providing a simple way to synchronize access to shared resources. This approach avoids race conditions by executing all tasks sequentially, but it may reduce concurrency and performance in scenarios where multiple reads could occur simultaneously.

Example:

let serialQueue = DispatchQueue(label: "com.example.serialQueue")

func updateVisitorCount() async {
await serialQueue.async {
visitorCount += 1
}
}

func getVisitorCount() async -> Int {
return await serialQueue.sync {
return visitorCount
}
}

In this scenario, even though the async keyword is used, tasks submitted to the serial queue are executed one after another, ensuring safe access to the visitorCount.

2. Concurrent Queues with DispatchBarrier​

For scenarios where you want to allow multiple threads to read a shared resource concurrently but need to synchronize write operations, using a DispatchBarrier is an effective solution. A DispatchBarrier ensures that when a write operation occurs, it has exclusive access to the resource, blocking all other reads and writes until it completes.

Example:

let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

func updateVisitorCount() async {
await concurrentQueue.async(flags: .barrier) {
visitorCount += 1
}
}

func getVisitorCount() async -> Int {
return await concurrentQueue.sync {
return visitorCount
}
}

Here, the DispatchBarrier blocks other tasks from executing while the visitorCount is being updated, preventing race conditions.

3. NSLock and Other Locking Mechanisms​

NSLock provides a low-level locking mechanism to ensure that only one thread can access a critical section of code at any given time. It’s a more granular approach compared to using queues, and can be useful when you need explicit control over synchronization.

Example:

let lock = NSLock()

func updateVisitorCount() async {
lock.lock()
visitorCount += 1
lock.unlock()
}

func getVisitorCount() async -> Int {
lock.lock()
defer { lock.unlock() }
return visitorCount
}

The await keyword here allows the function to be part of the async/await flow, but NSLock handles the actual synchronization.

4. @synchronized in Objective-C and Swift​

In mixed Objective-C and Swift projects, you might encounter the @synchronized directive, which works similarly to NSLock but provides more convenience by automatically unlocking when the synchronized block exits.

Example:

- (void)updateVisitorCount {
@synchronized(self) {
self.visitorCount += 1;
}
}

In Swift, achieving similar behavior would require manually implementing synchronization, as shown in the previous examples with NSLock.

5. Operation Queues and Dependencies​

OperationQueue provides a higher-level abstraction for concurrency, allowing you to define dependencies between operations, ensuring that certain tasks are completed before others start. This can effectively prevent race conditions by structuring the execution order of operations.

Example:

let operationQueue = OperationQueue()

let operation1 = BlockOperation {
// Read or write operation 1
}

let operation2 = BlockOperation {
// Read or write operation 2
}

// Make operation2 dependent on operation1
operation2.addDependency(operation1)

operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)

This allows for structured concurrency, where the order of operations ensures safe access to shared resources.

6. Readers-Writers Locks​

Readers-Writers Locks are specialized locks that allow multiple readers to access a resource simultaneously but give exclusive access to a writer. This approach maximizes read concurrency while ensuring write safety.

Example:

Swift doesn’t provide a built-in Readers-Writers Lock, but you can implement one using a combination of semaphores or condition variables to achieve similar behavior.

Example using DispatchSemaphore:

let readerWriterLock = DispatchSemaphore(value: 1)

func readResource() async {
readerWriterLock.wait()
defer { readerWriterLock.signal() }
// Perform read operation
}

func writeResource() async {
readerWriterLock.wait()
// Perform write operation
readerWriterLock.signal()
}

In this example, DispatchSemaphore controls access to the shared resource, ensuring that reads and writes are synchronized.

Integrating async/await with Race Condition Prevention​

The async/await model enhances code readability and manageability but doesn't inherently solve the problem of race conditions. The traditional mechanisms like serial queues, DispatchBarrier, NSLock, and OperationQueue still play a crucial role in ensuring thread safety. async/await simply allows these mechanisms to be used in a more straightforward and asynchronous manner, making it easier to handle concurrency without sacrificing safety.

By combining async/await with these traditional synchronization mechanisms, you can effectively prevent race conditions while maintaining the advantages of Swift's modern concurrency model.

Additional Considerations​

  • Task Priorities: When working with async/await, consider the priorities of your tasks, as higher-priority tasks might still cause race conditions if not properly synchronized.
  • Actor Model: Swift introduces the Actor model as a higher-level abstraction for protecting mutable state across multiple concurrent tasks, which can be considered when dealing with race conditions in more complex scenarios.

In conclusion, async/await simplifies asynchronous code but still requires careful management of shared resources to prevent race conditions. By integrating it with the synchronization techniques discussed, you can maintain safe, efficient, and readable code.

Additional Details​

  • Serial Queues: Simple and safe, but limit concurrency.
  • DispatchBarrier: Allows concurrent reads, synchronizes writes.
  • NSLock: Fine-grained control, but beware of deadlocks.
  • OperationQueue: Manage dependencies and order of execution.
  • Readers-Writers Locks: Maximize read concurrency, synchronize writes.
In Bullets
  • Serial Queues: Default synchronization by processing one task at a time.
  • DispatchBarrier: For concurrent queues, use DispatchBarrier to synchronize write operations.
  • Thread-Safe: Guarantees that no reading occurs during writing and vice versa.
  • Using Locks: NSLock provides fine-grained control over critical sections.
  • OperationQueue: High-level structure for managing task dependencies.

How can you cancel a running asynchronous task?

· 4 min read
Szymon Michalak
iOS Developer
Sources & Resources

Main Source: đź”— iOSwift.dev

Additional Sources:

Further Reading:

TL/DR

To cancel a running asynchronous task, you need to maintain a reference to the task. For DispatchQueue, use DispatchWorkItem; for OperationQueue, use Operation. With Swift's modern concurrency model, Task provides more integrated and straightforward cancellation handling. Canceling a task in any of these systems will not stop it immediately but will set its state to isCancelled, which the task must check to stop execution.

DispatchQueue​

Canceling a task on a DispatchQueue involves maintaining a reference to the DispatchWorkItem. Here’s how you can create and cancel a task:

let task = DispatchWorkItem { [weak self] in
print("Performing a task...")
}
DispatchQueue.main.async(execute: task)
task.cancel()

OperationQueue​

When using OperationQueue, tasks are managed by the queue itself. You can cancel a specific operation or all operations in the queue:

let operationQueue = OperationQueue()
let op1 = BlockOperation { print("First") }
let op2 = BlockOperation { print("Second") }
let op3 = BlockOperation { print("Third") }
operationQueue.addOperations([op1, op2, op3], waitUntilFinished: false)

// Pause / Resuming Operation Queue
operationQueue.isSuspended = true
if operationQueue.isSuspended {
operationQueue.isSuspended = false
}

// Cancels a single Operation
op2.cancel()
// Output: true
print(op2.isCancelled)

When you call cancel() on an Operation, it does not force the operation’s code to stop executing immediately. Instead, it updates the Operation’s isCancelled property to reflect this change in state. You need to check isCancelled within the operation’s code:

final class ExampleOperation: Operation {
override func main() {
guard !isCancelled else { return }
// Do something....
}
}

Task​

With Swift’s concurrency model, Task offers a more modern and integrated way to handle asynchronous work. Task handles its own cancellation more gracefully:

let task = Task {
for i in 0..<5 {
guard !Task.isCancelled else {
print("Task was cancelled")
return
}
print("Task running \(i)")
try await Task.sleep(nanoseconds: 1_000_000_000) // simulate work
}
}
task.cancel() // This will set the task as canceled

Comparison: OperationQueue vs. Task​

  • Concurrency Model:

    • OperationQueue: Part of the older Grand Central Dispatch (GCD) framework, requiring more manual setup.
    • Task: Integrated with Swift's modern concurrency model, offering a simpler and more elegant approach.
  • Ease of Use:

    • OperationQueue: More complex and verbose, especially for simple tasks.
    • Task: Simpler and more concise, especially with async/await.
  • Automatic Propagation of Cancellation:

    • Task: Automatically propagates cancellation to child tasks, simplifying cancellation management.
    • OperationQueue: Requires manual management of each operation's cancellation state.

Additional Details​

  • Task State Handling: Canceling an Operation or Task will affect it only if it is still queued or running. If the task has finished, cancellation has no effect.

    An Operation can be in one of the following states when cancel() is called:

    • Finished: If the Operation has already finished executing, calling cancel() has no effect.
    • Queued but Not Yet Running: Canceling an Operation in this state allows it to be removed from the queue earlier. The OperationQueue will check isCancelled before starting the task, and if true, it will exit immediately.
    • Currently Executing: If the Operation is currently executing, the isCancelled property is updated, but the operation’s code needs to explicitly check this property and decide how to handle the cancellation.
  • Task Self-Cancellation: Task automatically handles its own cancellation. By checking Task.isCancelled, you can determine whether the task should stop executing.

  • Queue Management: You can suspend and resume the entire OperationQueue, which affects all operations within it. You can also cancel all operations in the queue by calling cancelAllOperations().

operationQueue.isSuspended = true
if operationQueue.isSuspended {
operationQueue.isSuspended = false
}
  • Nuanced Control: Canceling an operation or task is not an immediate action. It requires the operation’s or task's code to respect the isCancelled property. This is crucial for scenarios where cleanup or partial completion might be necessary before fully stopping the operation or task.

In Bullets​

In Bullets
  • DispatchQueue: Use DispatchWorkItem to create a cancellable task.
  • OperationQueue: Use Operation objects and check isCancelled to manage task cancellation.
  • Task API: Swift's Task can automatically handle its own cancellation using Task.isCancelled.
  • Operation States: An Operation can be finished, queued, or executing when canceled, and the impact of cancellation depends on its current state.
  • Cancel Behavior: Canceling a task updates its state but doesn't immediately stop execution.
  • Queue Management: You can suspend, resume, and cancel tasks within an OperationQueue.

What are the differences between DispatchQueue, Operation, and Threads?

· 4 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

The differences between DispatchQueues, Operations, and Threads can be difficult to distinguish, so let's take a moment to review their similarities and differences.

Threads
Simply speaking, threads allow a process to split itself up into multiple simultaneously running tasks.

In iOS, you have what Apple calls a "pool of threads." This is a collection of threads, other than the Main Thread, that the system can assign work to. The operating system will delegate work to one or more of these threads depending on the number of other competing tasks, the load on the processor, and a variety of other environmental factors.

The threads in iOS are so heavily optimized that if a developer were to interact with or dispatch work to them directly, they would almost certainly degrade the thread's performance or introduce issues related to reusability or locking. As a result, Apple provides abstractions over these threads like DispatchQueue and OperationQueue for developers to use rather than tasking developers with implementing their own custom thread management logic.

When we use these services, we lose visibility into exactly what threads are assigned to execute our work, but this deliberate abstraction simplifies our work tremendously. It allows us to put our trust in iOS to look at all of the relevant factors and dispatch the work intelligently on our behalf.

The main takeaway here is that you should always delegate your work to a DispatchQueue or an OperationQueue instead of trying to interact withthe threads directly.

DispatchQueue / Grand Central Dispatch
A DispatchQueue, also known as Grand Central Dispatch(GCD), allows you to execute tasks either serially or concurrently on your app’s main and / or background threads. As a developer, you simply assign work to GCD and it will delegate it to one or more threads from the system's thread pool, but makes no promises or guarantees about which threads will be used.

DispatchQueues support both serial and concurrent execution. Serial queues synchronize access to a resource, whereas concurrent queues execute one or more tasks concurrently. In the case of a concurrent queue, tasks are completed according to their complexity and not their order in the queue.

In the simplest terms,DispatchQueueprovides a layerof convenience over the system's thread pool. The developer simply needs to define the task - a DispatchWorkItem - and ship it over to a DispatchQueue which will handle all of the heavylifting and thread management.

OperationQueue
You can think of OperationQueue as a better, more powerful version of DispatchQueue.

Grand Central Dispatch (GCD) is a low-level C API that interacts directly with the Unix level of the system whereasOperationQueueis built on topof GCD and provides an Objective-C interface instead.

As we will discuss in a moment, OperationQueue comes with many advantages, but it also introduces some additional overhead. This is due to the fact that OperationQueue instances need to be allocated and deallocated whenever they are used. Although this process is heavily optimized, this additional overhead makes it slower than GCD.

So, what's the advantage of OperationQueue if it's slower than GCD?OperationQueue allows you to manage scheduled operations with much greater control than GCD provides. OperationQueues make it easy to specify dependencies between individual operations, to cancel and suspend enqueued operations, or to re-use operations, none of which are possible with GCD. Apple's official recommendation is to always use the highest level of abstraction available, but if you only need to dispatch a block of code to execute in the background and you don't necessarily needOperationQueues’additional features,I thinkDispatchQueueis a reasonable choice. If you need more functionality, you can always upgrade to anOperationQueue`.

What does deadlock mean?

· 2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Deadlocks occur when threads sharing the same resource are waiting on one another to release access to the resource in order for them to complete their respective tasks.

Interviewer: “Explain deadlock and I’ll give you the job.”
Candidate: “Give me the job and I’ll explain deadlock to you.”

Simply put, deadlock describes a situation in which you have two or more entities waiting on one another and, as a result, no progress is made. A deadlock can occur between any two entities. For instance, your code could be waiting for a file system operation to complete and the file system could be waiting for your code to complete execution.

Here’s a simple example of a deadlock:

func deadLock() {
let queue = DispatchQueue(label: "deadlock-demo")

queue.async { // A
queue.sync { // B
print("B: Waiting on A to finish.")
}
print("A: Waiting on B to finish.")
}
}

Our queue is a serial queue so it’s going to run these blocks synchronously - one after the other.

In this case, the inner closure (B) can’t run until the outer closure (A) finishes. This is because A is holding the control of the current thread (remember it’s a serial queue). A can never finish its execution because it’s now waiting for B to finish running.

A depends on B and B depends on A and the thread stalls. One solution would be to change the queue type from serial to concurrent. This would allow the inner closure, B, to start without waiting for A to finish.

Be careful when working with serial queues as they can easily lead to deadlocks.

What does it mean for something to be thread safe?

· 2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Thread safety means that code functions correctly during simultaneous execution by one or more threads. It ensures that a shared resource can only be modified by one thread at a time, preventing unpredictable behavior and race conditions.

Thread safety is crucial in concurrent programming. Code is considered thread-safe when it functions correctly during simultaneous execution by one or more threads. More specifically, thread safety ensures that some shared resource can only be modified by one thread at any given time.

Consider a scenario where your application has multiple threads all trying to access the same shared resource. Some threads may simply be attempting to read from the resource while others are trying to modify it.

Now, the resulting state of your application is entirely dependent on whichever thread happens to finish execution first. In software development, we strive for determinism wherever possible.

Imagine a situation where two threads are attempting to modify the same document. In such a scenario, it’s possible for one thread to overwrite the changes made by the other. To make this operation thread-safe, you have to ensure synchronized access to the resource. In other words, you’ll need to make all of the other threads wait until the current thread modifying the resource finishes execution.

Keep in mind that the ordering of the threads' access to some shared resource is determined by the operating system’s scheduler. Since the scheduler dispatches tasks based on the current resource availability, task priority, etc., it may order operations differently from moment to moment. There’s no guarantee that the threads will always execute their tasks in a particular order, leading to potential race conditions.

Thread safety is generally discussed in relation to modifying a resource because multiple threads can read from a resource at the same time without causing changes to the resource, which typically doesn’t introduce issues.

In Bullets
  • Thread Safety: Ensures that code functions correctly during simultaneous execution by multiple threads.
  • Shared Resource: Only one thread can modify a shared resource at any time, preventing race conditions.
  • Synchronized Access: Necessary to ensure that modifications by multiple threads don’t conflict with each other.
  • Scheduler: Manages thread execution order, which can lead to race conditions if not handled properly.

What is a race condition?

· 2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

A race condition occurs when there are multiple threads accessing the same memory concurrently and at least one of the threads is trying to update the information stored there (a.k.a a write task).

Imagine we had 3 threads, A, B, and C all accessing the same memory. In this example, A & B are simply reading the value at this location while C is trying to update the value stored there.

The order in which each thread completes their task can greatly influence the results of the other threads.

For example, if the task running on C finishes prior to the tasks on A & B, then A & B will both be seeing the updated value. Otherwise, if C finishes afterwards, A & B will have completed their tasks on what is now out of date information.

The order in which these threads complete their task is entirely dependent on the operating system’s available resources, the respective thread’s priority level, and in how the operating system’s scheduler schedules these operations.

As you can imagine, this would introduce some variability with our code’s execution and we now have a race condition on our hands; whichever thread happens to finish first can wildly and unpredictably influence the state of our application.

If all 3 threads were just reading the value, we’d have no issues.

Fortunately, Xcode provides us with the Thread Sanitizer tool to help us debug issues involving race conditions. This tool can be enabled by editing your target’s scheme and will detect race conditions at runtime.

What is Behavior Driven Development?

· 3 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Behavior Driven Development (BDD) is a software development methodology that aims to document and design an application based around the behavior the end user is expected to experience. In other words, it is behavior and results focused instead of implementation focused.

For example, when you’re writing traditional unit tests, you’ll often write tests for all of the various inputs a particular function could expect to receive and assert that the result matches what you’d expect.

This approach though is entirely focused on the implementation details of your application. As a result, you spend more time testing particulars of the implementation over the actual business logic of your application. BDD strives to tip the balance in the other direction by focusing on testing what your application does insteadof how it does it.

In BDD, when writing tests, you’ll start with a user story and model your tests around the expected behavior for the end user. While Swift doesn’t support writing BDD style tests natively, popular frameworks like Quick & Nimble allow us to add BDD style testing to the iOS ecosystem.

Here’s an example test case written in the BDD style - notice that we’re focusing on testing the end result instead of how we get there:

describe("the favorite button") {
it("is selected if the participant is already a favorite") {
favorite = TestUtils.fakeFavorite()
createFavoritesViewController()
expect(viewController.favoriteButton.isFavorited).to(beTrue())
}

it("is not selected if the participant is not already a favorite") {
favorite = nil
createFavoritesViewController()
expect(viewController.favoriteButton.isFavorited).to(beFalse())
}
}

describe("Email/Password Authentication") {
context("when user presses sign in") {
it("shows login view") {
homeScreenViewController.signInButton
.sendActions(for: .touchUpInside)
expect(navigationControler.topViewController)
.toEventually(beAKindOf(LoginViewController.self))
}
}

context("when user presses sign up") {
it("shows sign up and interactor not called") {
homeScreenViewController.registerButton
.sendActions(for: .touchUpInside)
expect(navigationControler.topViewController)
.toEventually(beAKindOf(RegisterViewController.self))
}
}
}

BDD tests follow the Given, When, and Then format.

If we consider the examples above, we start with the describekeyword which clarifies the action or behavior we’re looking to test and establishes any preconditions for the test given.

Next, the context block describes the conditions inwhich this behavior should be expected when.

Finally, the it block specifies the expected behaviorand validates the results then.

Typically, you’d start with the user story and work backwards to fill out these sections.

This style of writing tests very closely models written language and as a result BDD style tests are often more readable and easily maintained than their unit testing counterparts. This style of testing also makes the expected behavior extremely clear and is reasonably self-documenting.

What is the difference between a serial and a concurrent queue?

· 2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

We can create both types of queues with the help of DispatchQueue which is built on top of Grand Central Dispatch.

In the case of a serial queue, the tasks will complete in the order they are added to the queue (FIFO); the first task in the queue will complete before the next task begins.

Serial queues are often used to provide synchronized access to a shared resource in order to prevent race conditions.

let serialQueue = DispatchQueue(label: "aryamansharda")

serialQueue.async {
print("This happens first!")
}

serialQueue.async {
print("This happens second!")
}

As you’d expect, the output will be “This happens first!” and then “This happens second!”.

On the other hand, a concurrent queue allows us to start multiple tasks at the same time. However, while we’re starting all of these tasks at the same time, a concurrent queue makes no guarantee about the order in which the tasks willfinish as these tasks are run in parallel.

If you need to establish dependencies between operations or need to ensure a completion order, consider using an OperationQueue or a serialqueue instead.

let concurrentQueue = DispatchQueue(label: "aryamansharda",
attributes: .concurrent)

concurrentQueue.async {
print("I'm added to the queue first!")
}

concurrentQueue.async {
print("I'm added to the queue second!")
}

Even though we’re adding tasks sequentially, it’s plausible for the second task to finish before the first one completes. It’s simply a matter of how the system dispatches the work to the underlying threads and their respective workloads.