Skip to main content

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.

How can you create a method with default values for its parameters?

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

Main Source: đź”— Ace the iOS Interview

TL/DR

When we declare a function in Swift, we can specify defaults for our parameters by specifying values within the method declaration.

When we declare a function in Swift, we can specify defaults for our parameters by specifying values within the method declaration:

func sayHello(name: String = "reader")

Now, we can call this function directly without having to explicitly specify any parameters as the default values will be used instead.

If the function contains parameters that don’t have a default value specified, you’ll need to specify a value for that parameter as usual. Otherwise, the compiler will return a “missing argument” error.

Given this example function:

func sayHello(name: String = "reader") {
print("Hello, \(name)")
}

These two function calls are equivalent:

sayHello() 
sayHello(name: "reader")

Now, let’s consider this function declaration:

func logStatement(prettyPrint: Bool = false, includeTimestamp: Bool, enableVerboseMode: Bool = false, message: String) {}

As you can see, there are several parameters with default values specified, but includeTimestamp and message are explicitly required. When we create functions with a mix of parameters like this, Xcode’s auto-complete will help enumerate all of the valid variations of the call to our function.

In Bullets
  • Default Parameters: Swift allows you to specify default values for parameters within the function declaration.
  • Flexibility in Function Calls: If parameters have default values, they can be omitted when calling the function.
  • Required Parameters: Parameters without default values must be explicitly provided, otherwise, the compiler will throw an error.

How do we provide default implementations for protocol methods?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

By leveraging Swift extensions, we can provide a default implementation for methods and properties declared in a protocol.

This helps reduce boilerplate and duplicated code in classes that implement the protocol while still allowing them to easily override the default implementation.

protocol Animal {
func makeNoise()
}

extension Animal {
func makeNoise() {
print("Bark!")
}
}

struct Dog: Animal {}
struct Cat: Animal {
func makeNoise() {
print("Meow!")
}
}

let sparky = Dog()
sparky.makeNoise() // Bark!

let whiskers = Cat()
whiskers.makeNoise() // Meow!

As you can see, we’ve provided the default implementation for makeNoise()in an extension.

Dog is using the default implementation while Cat is free to provide a more specific implementation.

This same approach allows us to make certain functions in our protocol optional. Since we have provided a default implementation in our extension, any entity that implements the protocolis no longer required to implement it.

protocol MyProtocol {
func doSomething()
}

extension MyProtocol {
func doSomething() {
/* Return a default value or just leave empty */
}
}

struct MyStruct: MyProtocol {
/* No compile error */
}

Alternatively, you can use @objc optional to make functions within your protocol optional. This would, however, restrict your protocol to only be implemented by class type objects, which would prevent your protocol from being used by structs, enums, etc. You’d also need to explicitly check if that optional method is implemented before you call it.

How do you create interoperability between Objective-C and Swift?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

If you’re applying for an older company like YouTube, Facebook, or even Google, you’ll likely have to work with both Objective-C and Swift within the same project.

As part of that workflow, it’s useful to know how to expose variables, classes, and methods declared in Swift to the Objective-C runtime and vice-versa.

Exposing Objective-C to Swift To import a set of Objective-C files into Swift code within the same app target, you rely on an Objective-C Bridging Header file to expose those files to Swift. Xcode offers to create this header for you when you add a Swift file to an existing Objective-C app or an Objective-C file to an existing Swift app.

When you accept, Xcode creates the Bridging Header file and names it by using your product module’s name followed by "-Bridging-Header.h" (e.x.“Facebook-Bridging-Header.h”).

In your newly created Bridging Header file, you can specify all of the Objective-C headers you want to expose to Swift like this:

#import "Properties.h"
#import "Auth.h"

Now, these Objective-C entities are automatically available in any Swift file within the same target without the use of any additional import statements.

Exposing Swift to Objective-C Now, what if you wrote a cool extension in Swift that you want to have access to in Objective-C?

You can work with types declared in Swift from within the Objective-C code in your project by importing an Xcode-generated header file (e.x. “ProductModuleName-Swift.h").This file is an Objective-C header that declares the Swift interfaces in your target.

You don’t need to do anything special to use the generated header - just import it in the Objective-C classes as needed:

#import "ProductModuleName-Swift.h"

This header file is managed behind the scenes. As a result, you don’t need to specify each individual class you want to expose to Objective-C like we did in the previous section.

You can think of it as an umbrella header for all of your Swift code.

By default, the generated header contains interfaces for Swift declarations marked with the public or open modifier. If your app target has an Objective-C bridging header, the generated header also includes interfaces for resources marked with the internal modifier. Declarations marked with the private or fileprivate modifier don'tappear in the generated header and aren't exposed to the Objective-C runtime unless they are explicitly marked with a @IBAction, @IBOutlet, or @objcattribute.

How do you use the Result type?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

The Result type is a convenient way for us to handle both the success and failure cases of an operation while maintaining code readability.

Under the hood, the Result type, is an enum with two cases:

enum Result<Success, Failure> where Failure : Error {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
}

The success case accepts any generic Success type(includingVoid) and failure takes the generic Failure type as its associated value.

Note: Whatever you use for the Failure’s associatedvalue must implement the Error protocol.

In the example below, you’ll see that we’re creating a PrimeNumberError enum with multiple cases. This approach allows us to have much greater specificity with our error messaging and has the added benefit of forcing us to handle each error case explicitly.

enum PrimeNumberError: Error {
case zero
case negative
case tooBig
}

func isPrimeNumber(num: Int) -> Result<Bool, PrimeNumberError> {
guard num < 0 else {
return .failure(.negative)
}

guard num > 0 else {
return .failure(.zero)
}

guard num < 1000 else {
return .failure(.tooBig)
}

return .success(primeNumberChecker(num: num))
}

switch isPrimeNumber(num: 23 ) {
case .success(let isPrime):
print("The number \(isPrime? "is" : "is not") prime")
case .failure(.tooBig):
print("The number is too big for this function.")
case .failure(.negative):
print("A prime number can't be negative.")
case .failure(.zero):
print("A prime number has to be greater than 1.")
}

Result is available in Swift 5.0+.

How does code signing work?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Code signing is a process that helps you verify the authenticity of the app or software you’re installing and ensures that it hasn’t been tampered with since the developer released it. The code signing process consists of a series of smaller steps: requesting a certificate, receiving a certificate, generating a provisioning profile, and finally signing the application.

Let’s start with requesting a certificate.

To request a certificate, we’ll need to create a certificate signing request - a .csr file. When you first set up your Apple Developer account, you will need to use Keychain Access to request a development certificate from Apple - the certificate authority.

A certificate authority is the entity responsible for issuing digital certificates. Simply put, it’s a trusted organization responsible for verifying the authenticity of software and other digital goods.

Since you have most likely already experienced this certificate request flow, I won't provide a tutorial for that process here. Instead, I'd like to discuss what's happening under the hood.

When you start the process of creating your certificate signing request, Keychain Access will generate a public and private key on your local machine. The public key is eventually attached to the .csr file, but the private key never leaves your machine. After this step, Keychain Access will prompt you to provide some basic metadata like name, country, email, etc. thereby completing the creation of the request.

From here, Apple examines the request's properties and metadata to verify who is requesting the certificate. Upon successful validation, Apple will send you back a certificate, which you should store in your machine's keychain. Certificates usually last for a year before expiring and can come in several varieties; iOS App Development, iOS Distribution, Mac App Distribution, Mac Installer Distribution, etc.

Code signing is driven by asymmetric cryptography, which is what enables communication with Apple and powers the signing process. Any explanation of code signing would be incomplete without an explanation of this encryption, so let's take a quick look at how it works.

To start with, both you and Apple have your own set of public and private keys. Now, these keys are cryptographically linked meaning you can use the private key to decrypt a message encrypted with the public key, but you can’t go in the other direction.

After establishing a connection with Apple, we will exchange public keys. The public keys are intended to be shared, but the private keys need to be protected.

So, when you want to send a message to Apple, you encrypt the message with Apple's public key. Then, when Apple receives the message, they’ll be able to decrypt the message using their private key.

This same process happens in reverse when Apple sends the certificate to us; Apple will encrypt the message using our public key and we’ll use our private key to decrypt the message and retrieve the certificate. This process ensures that our full conversation with Apple from requesting to receiving the certificate is secure.

As a next step, the provisioning profile is created, consisting of a few key components:

  • Team ID: A unique identifier for each developmentteam and can be found in your Apple Developer account.
  • Bundle ID: Every iOS app has a unique bundle identifierwhich allows it to be uniquely identified.
  • App ID: The combination of the Team ID and the BundleID.
  • Device ID: The list of all UDIDs (Unique Device Identifier)that your iOS application is authorized to run on. This is a 40 character alphanumeric identifier.
  • Entitlements: This specifies the permissions and capabilitiesof the app along with which system resources the application has permission to access. For example, services like Push Notifications, Apple Pay, App Sandbox, etc.
  • Certificate: The certificate we received from Applein the previous step (iOS App Development, iOS Distribution, Mac App Distribution, Mac installer Distribution, etc.)

In summary, the provisioning profile is composed of the certificate that verifies the authenticity of the software, the App ID that uniquely identifies the application, and its permissions, entitlements, and the exact list of devices that can run the application. It essentially acts as the middle-man between the end devices and the developer account.

We're finally at the last step - code signing. This step involves downloading the provisioning profile from your developer account and embedding it into your application's bundle. Next, the bundle is signed with the certificate we created earlier and can now run on your device.

Here’s what’s happening under the hood:

  1. The certificate referenced in your provisioning profile is compared against the available certificates in your machine’s keychain.
  2. If a valid certificate is found, it is used to sign the executable.
  3. The UDID of the device you are attempting to run the executable on is compared against the UDIDs listed in the provisioning profile.
  4. The Bundle ID and Entitlements are checked against their respective counterparts in the provisioning profile.
  5. If everything goes well, the app is installed on the device.
In Bullets

How would we test the performance of a method in our tests?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

The XCTest framework includes a measure() function for profiling code execution times. We can call this method within our testing target to measure the performance of a block of code:

func testExample() throws {
measure {
sayHelloWorld()
}
}

By default, this method counts the number of seconds it takes to execute a block of code.

To customize the metrics measure() function tracks, we can override defaultPerformanceMetrics[] and specify our own metrics to track.

The measure() function runs the code within the closure ten times and reports the average execution time and standard deviation. These numbers will now serve as the benchmarks for all future runs of this test.

On a subsequent run, if the execution time is within 10% of the benchmark value, the test will pass. Otherwise, the test will fail. This can help us catch performance problems early on in the development process.

If we were to improve the performance of the code we're profiling, we can simply update the baseline measurement, and now all subsequent executions will be compared against this new value.

Xcode saves benchmark numbers into source control which allows them to be standardized across teams.

Finally, the benchmark numbers are device-specific. This ensures that Xcode will not fail the test when the iPhone 6 performs worse than the iPhone 13.

How would you animate a view that has a constraint?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

With AutoLayout, you can simply change a view’s constraints and let the system figure out how to resize that view and all of its neighbors.

In interviews, I often see candidates mix AutoLayout constraint changes with manual changes to a view’s frame. This combination of two different layout paradigms really complicates the logic and is often error-prone. I’d recommend you stick with AutoLayout whenever possible.

Now, assuming you have a reference to the constraint you want to manipulate, you can simply animate a change by updating the constraint’s value like so:

imageViewHeightConstraint.constant = 80

UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}

Make sure you update the constraint outside of the animation block!

Remember to call layoutIfNeeded() on self.view and not on the view you are attempting to animate. Otherwise, the changes in layout will be applied without animation.

Additionally, Apple recommends calling layoutIfNeeded() once before the animation block to ensure all pending layout operations are completed.

It’s very important that we call layoutIfNeeded() on self.view and not just on the view we are trying to animate. By calling this function on self.view,the animation and layout changes will “trickle down” through all of the other subviews.

Remember, you want to animate changes to the neighboring view’s layouts as well - not just changes to any one particular subview.

How would you avoid retain cycles in a closure?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

This will build off our understanding of ARC from the previous questions.

Often times, closures will introduce retain cycles. Since a closure is a reference type it maintains a strong reference to all of the objects referenced in the body of the closure thereby increasing their retain count.

To manage this, we can use a capture list. This allows us to explicitly specify which objects we want to maintain a reference to, but more importantly whether we want those references to be weak, strong, or unowned.

We can pick weak or unowned(where applicable) to ensure that we can still reference all of the objects the closure needs, but we don’t inadvertently increase their retain count and introduce a retain cycle.

The following example has a retain cycle as the body of the closure creates a strong reference to isUserActive and isUserOnlineView.

class RetainCycleDemo {
@IBOutlet var isUserOnlineView: UIView!

var isUserActive = false

func setUserActivityStatusView() {
userService.checkUserOnlineStatus { isOnline in
self.isUserActive = isOnline

if isOnline {
self.isUserOnlineView.backgroundColor = .green
} else {
self.isUserOnlineView.backgroundColor = .red
}
}
}
}

Fortunately, we can fix this by simply using a weak reference to self instead. And, voila - no more retain cycles!

class RetainCycleDemo {
@IBOutlet var isUserOnlineView: UIView!
var isUserActive = false

func setUserActivityStatusView() {
userService.checkUserOnlineStatus { [weak self] isOnline in
self?.isUserActive = isOnline
if isOnline {
self?.isUserOnlineView.backgroundColor = .green
} else {
self?.isUserOnlineView.backgroundColor = .red
}
}
}
}

How would you debug view layout issues?

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

Main Source: đź”— Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

As an iOS developer, layout problems are an inevitability. From breaking constraints to weird UI issues occurring only at runtime (text truncation, alpha value issues, broken animations, etc.), debugging layout issues in iOS can be tricky.

Therefore, it’s crucial to know all of the different approaches to debugging layout issues in iOS.

This is particularly useful during an interview when time is of the essence. Moreover, it's a great way to demonstrate your familiarity with advanced Xcode features.

Debug View Hierarchy

The Debug View Hierarchy pauses the application in its current state thereby providing the programmer an opportunity to inspect and better understand the UI hierarchy of their app.

This view will not only show you an “exploded” 3D version of your view, but it helps you understand the full hierarchy of the view from the top most view controllers all the way down to individual subviews,UILabels,UIImageViews, etc.Additionally, this tool will also highlight anyUIView’s with runtime constraint errors.

There’s a lot of functionality here, so I’d recommend spending some time playing around with it if you’re unfamiliar. It can help you catch breaking constraints, clipped views, and a variety of other layout issues.

Customizing Constraint Identifiers

Troubleshooting constraint issues is particularly challenging because the error messages are not very user-friendly.

To make things easier, we can leverage theidentifierproperty on a NSLayoutConstraint.

This property is available to use regardless of whether the constraint is defined through a .storyboard, .xib, or programmatically.

var bannerWidthConstraint: NSLayoutConstraint?
bannerWidthConstraint.identifier = "Promotional bannerwidth"

A custom identifier makes it easier for you to distinguish between system-generated and user-generated constraints in Debug Logs.

By leveraging custom identifiers, the Debugger output will now contain clearer error messages which will make it much easier to track down and resolve layout issues.

Without Identifier:

Will attempt to recover by breaking constraint

<NSLayoutConstraint:0x7a87b000 H:[UILabel:0x7a8724b0'Name'(>=400)]>

With Identifier:

Will attempt to recover by breaking constraint

<NSLayoutConstraint:0x7b56d020 'Label Width' H:[UILabel:0x7b58b040'Name'(>=400)]>

As you can see, these identifiers allow you to quickly and easily identify specific constraints in the log output.

exerciseAmbiguityInLayout()

This method randomly changes the frame of a view with an ambiguous layout between its different valid values, causing the view to move in the interface. This makes it easy to visually identify what the different valid frame configurations are and helps the developer understand what constraints need to be added to the layout to correctly and fully specify the layout of the view.

This method should only be used for debugging purposes; no application should ship with calls to this method.

Developer Tool: đź”— https://www.wtfautolayout.com/

If you’re having difficulty making sense of the Debugger’s “breaking constraint” output, you can use this site to help you easily visualize the problem.