Skip to main content

Can we use Swift’s reserved keywords as variable or constant names?

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

Main Source: 🔗 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Yes and it’s accomplished through the use of backticks.

If you want to use a reserved keyword, for example as a case in an enum, you can just add backticks around the reserved keyword:

enum MembershipType {
case `default`
case premium
case trial
}

// Free to use MembershipType.default now

Otherwise, without the backticks, we’d have a compilation issue.

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.

Can you explain what the @objc keyword does?

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

The @objc keyword makes Swift code accessible to Objective-C.

If you’re applying to an older company, they’ll likely have a substantial part of their codebase still in Objective-C. So, you’ll need to be comfortable with using both Objective-C and Swift in the same project.

This attribute is used to make your Swift code accessible to the Objective-C runtime.

Anytime you have a Swift class, property, or protocol that you want to access in Objective-C code, you’ll need to prefix it with this keyword.

// The ViewController is accessible in an Objective-C environment
@objc class ViewController: UIViewController {

// Visible in Objective-C
@objc var username: String!

// Not visible in Objective-C
private var password: String!

override func viewDidLoad() {
super.viewDidLoad()
}
}

@objc, when added to a class declaration, only exposes the public init. You will have to add it manually to all other properties and methods you want to expose.

If you want to expose all public properties and methods to Objective-C, you can use @objcMembers instead.

In Bullets
  • Purpose:

    • The @objc keyword is used to expose Swift code to the Objective-C runtime.
  • Usage:

    • Classes: Annotate Swift classes with @objc to make them accessible from Objective-C.
      @objc class MyClass: NSObject {
      // This class can be accessed from Objective-C code
      }
    • Methods: Use @objc for methods you want to be callable from Objective-C.
      @objc func myMethod() {
      // This method can be called from Objective-C
      }
    • Properties: Apply @objc to properties that need to be accessed in Objective-C.
      @objc var myProperty: String?
  • Limitations:

    • Private Members: Properties and methods marked private are not exposed to Objective-C even if they are annotated with @objc.
      @objc private var secret: String // Not visible in Objective-C
  • Objective-C Compatibility:

    • Enables interoperability between Swift and Objective-C, essential for projects with mixed-language codebases.

Do all elements in a tuple need to be the same type?

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

Main Source: 🔗 Ace the iOS Interview

Additional Sources:

Further Reading:

Tuples are a very convenient way to group elements together without having to create a custom object or struct to encapsulate them.

To create a tuple in Swift, simply specify a comma separated list of values within a set of parentheses like this:

let tuple = ( 1 , 2 , 3 , 123.0, "Hello, world!")
print(tuple. 4 ) // Hello, world!

As you can see, a tuple doesn’t have to be a homogenous set of types. It can easily be a mix of different types, but it’s up to you to keep track of what data type exists at each position and interact with it accordingly.

Does Swift support implicit casting between data types?

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

Main Source: 🔗 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Swift does not support implicit casting.

Swift does not support implicit casting.

Take the following expression involving Doubles, Floats, and Integers:

let seconds: Double = 60
let minutes = 60 // minutes is inferred to be an Int
let hours: Float = 24.0
let daysInYear: Int = 365

// Fails with "Cannot convert value of type Float / Double to
// expected argument type Int"
let secondsInYear = seconds * minutes * hours * daysInYear

// You can see that we've had to explicitly cast all of the
// non-Integer types to Int
let secondsInYear = Int(seconds) * minutes * Int(hours) * daysInYear
print(secondsInYear) //31536000

When you have an expression with multiple types, Swift will not automatically convert them to a common shared type. Instead, Swift forces you to be explicit about how you want to deal with this mix of types.

Since the output can only be of one type, Swift leaves it up to the programmer to define what that should be.

How are Content Hugging and Content Compression Resistance different?

· 3 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources
TL/DR
  • Content Hugging: Controls how much a view resists being stretched beyond its natural size. Higher priority = more resistance to growth.
  • Content Compression Resistance: Controls how much a view resists being shrunk below its natural size. Higher priority = more resistance to shrinking.

At its core, AutoLayout is a constraint solver. It will take some number of views and their constraints and try to formulate an arrangement that satisfies all of them.

Sometimes in this process, AutoLayout will make a view smaller than you’d like or may make it larger than you intend it to be.

In situations like this, we can leverage Content Hugging and Content Compression Resistance for more granular control over how AutoLayout resizes our views.

In order to understand how they work, we need to understand intrinsic content size. This is the minimum size views want to be to show all of their content. For example, views like UIImageViews, UIButtons, and UILabels all know what their size should be in order to accommodate the content they’re meant to show.

Content Hugging Resistance represents how hard a view is going to fight against being made larger than its intrinsic content size. The higher the priority, the harder it’s going to resist growing.

Conversely, Content Compression Resistance represents how hard a view is going to fight against being made smaller than its intrinsic content size. The higher the priority, the harder it's going to resist shrinking.

So, when AutoLayout is trying to resolve constraints, it’s going to look at these properties and their respective priorities to figure out which views it can make larger and which views it can make smaller.

In Bullets
  • Content Hugging

    • Definition: Measures how strongly a view resists growing larger than its intrinsic content size.
    • Priority Impact: Higher priority means the view resists being stretched more.
    • Example: A UILabel with high content hugging priority will avoid expanding beyond its text's natural width.
  • Content Compression Resistance

    • Definition: Measures how strongly a view resists shrinking smaller than its intrinsic content size.
    • Priority Impact: Higher priority means the view resists being compressed more.
    • Example: A UIButton with high content compression resistance will avoid collapsing below its text's natural size.
  • Usage in AutoLayout

    • AutoLayout uses these properties to balance view resizing based on constraints.
    • Content Hugging and Compression Resistance priorities guide how views are resized when constraints are ambiguous or conflicting.

How are Optionals implemented?

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

Optionals in Swift are implemented as enums with two cases: some, which holds an associated value, and none, which represents a nil value.

Optionals in Swift are a powerful feature that allow you to handle the absence of a value. Under the hood, Optionals are implemented as an enum with two cases: some, which contains an associated value, and none, which represents the absence of a value.

Here’s a simplified version of Swift’s Optional implementation:

public enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)

public init(_ some: Wrapped) {
self = .some(some)
}

public init(nilLiteral: ()) {
self = .none
}
}

In the case of .some, there's an associated value, which is the non-nil case of an Optional variable. This enum-based structure leverages Swift’s features like generics, associated values, and value semantics, enabling developers to build expressive and powerful constructs.

Knowing that an Optional is simply an enum opens up possibilities for extending Optionals with custom behavior and convenience methods.

Additional Details

Understanding the implementation of Optionals allows developers to create custom extensions on Optionals, adding additional behavior and convenience methods. It also emphasizes the importance of value types and enums in building robust language features.

extension Optional {
func isSome() -> Bool {
if case .some = self {
return true
}
return false
}
}
In Bullets
  • Optionals as Enums: Swift Optionals are implemented as enums with two cases: some and none.
  • Associated Values: The some case holds an associated value, representing a non-nil value.
  • Custom Extensions: Understanding this allows for creating custom extensions to enhance Optional's functionality.

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 limit a protocol conformance to a specific class or type?

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

Main Source: 🔗 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Swift allows us to limit a protocol's conformance to a specific class or type by using the where keyword or by inheriting from a base class. This restriction can also be applied in protocol extensions to provide default implementations for specific types.

When designing protocols in Swift, there may be situations where you want to restrict the types that can conform to a protocol. For example, you might want only certain subclasses or types that conform to other protocols to adopt your custom protocol. This can be achieved in two primary ways:

  1. Inheriting from a Base Class: You can restrict protocol conformance by specifying a base class that the conforming types must inherit from.

    protocol TestViewController: UIViewController { }

    In this case, only subclasses of UIViewController can conform to the TestViewController protocol.

  2. Using the where Keyword: Another way to limit conformance is by using the where clause in your protocol declaration.

    protocol TestViewControllerAlternative where Self: UIViewController { }

    This is similar to inheriting from a base class but provides more flexibility and can be used in various contexts, such as with protocol extensions.

Example: Restricting Protocol Conformance in Extensions

You can also use these restrictions in protocol extensions to provide default implementations only for specific types.

protocol Animal {
func speak()
}

class Dog: Animal {}
class Cat: Animal {}

extension Animal where Self: Dog {
func speak() {
print("Woof!")
}
}

extension Animal where Self: Cat {
func speak() {
print("Meow!")
}
}

In the above example, the speak() method is only implemented for Dog and Cat types.

Limiting Conformance to Types that Conform to Another Protocol

If you want your protocol to only be adoptable by types that conform to another protocol, you can also enforce this using the where clause.

protocol SecureHashable: Hashable {
var secureHash: String { get }
}

Here, only types that already conform to Hashable can conform to SecureHashable.

Additional Details

Restricting protocol conformance is particularly useful in complex systems where you want to ensure certain behaviors are only available to specific types. This approach can help prevent misuse of protocols and provide a cleaner, more predictable API.

// Additional example: Restricting to classes that conform to another protocol
protocol LoggedInView where Self: UIViewController & UserSessionHandler {
func showLoggedInView()
}
In Bullets
  • Restricting with a Base Class: You can limit protocol conformance by specifying a base class, such as UIViewController.
  • Using where: The where clause adds flexibility, allowing for restrictions based on class or protocol conformance.
  • Selective Extensions: Apply protocol extensions conditionally to provide default implementations only for certain types.

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.