iOS Interview Questions: Swift

Updated Nov 07, 2022#ios#swift#interview#questions

Preparing for an iOS interview requires you to take considerable amount of time to review Swift language features. Try to skim as many following questions as possible, you might not remember every possible question but at least heard about it.

Why is Swift a type-safe language?

Swift encourages you to be clear about the types of values your code can work with. It performs type checks when compiling your code and flags any mismatched types as errors. This enables you to catch and fix errors as early as possible in the development process.

What are Swift optionals?

func foo(bar: String?) {
  print(bar ?? "default value if missing")
}

Swift optionals let you indicate the absence of a value for any type at all, without the need for special constants. Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.

The concept of optionals doesn’t exist in Objective-C. The nearest thing in Objective-C is the ability to return nil from a method that would otherwise return an object, with nil meaning the absence of a valid object.

Difference between assertions and preconditions?

func foo(bar: Int) {
  assert(bar > 1, "bar must be greater than 1")
  /* ... */
}

Known from many programming languages, the assert() is evaluated only in debug mode, will terminate application if condition not met. For release builds, lines with assert() are ommited (condition is not evaluated).

func foo(bar: Int) {
  precondition(bar > 1, "bar must be greater than 1")
  /* ... */
}

The precondition() ensures that the given condition is checked for both debug and release builds, will terminate the application if condition not met. For unchecked optimization level (-Ounchecked) optimizer will assume that condition is always met.

Why immutability is important in Swift?

Immutability helps safety and performance. Immutable objects are useful in multi-threaded applications because multiple threads can act on the data of immutable objects without worrying about changes to the data by other threads.

As immutable objects are closed to change, it is safe to assume that they will stay unchanged while we access the object from different threads. This assumption simplifies most of the multithreading problems that are complex to solve and maintain. For instance, we do not need to think about synchronization/locking mechanisms at all.

Why were C-style for-loops removed in Swift 3?

let items = ["foo", "bar"]
for (var i = 0; i < items.count; i += 1) {
  print(items[i])
}

The C-style for-loop appears to be a mechanical carry-over from C rather than a genuinely Swift-specific construct. It is rarely used and not very Swift-like.

More Swift-typical construction is already available with for-in statements and stride. Removing for loops would simplify the language and starve the most common use-points for -- and ++, which are already due to be eliminated from the language.

let items = ["foo", "bar"]

for item in items {
  print(item)
}

for i in 0..<items.count {
  print(items[i])
}

for (index, item) in items.enumerate() {
  print("\(index): \(item)")
}

What are types of closures?

Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.

Closures can capture and store references to any constants and variables from the context in which they’re defined. Closures take one of three forms:

  • Global functions are closures that have a name and don’t capture any values.
  • Nested functions are closures that have a name and can capture values from their enclosing function.
  • Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.

Difference between structures and classes?

Structures and classes are general-purpose, flexible constructs that have many things in common.

  • Define properties to store values.
  • Define methods to provide functionality.
  • Define subscripts to provide access to their values using subscript syntax.
  • Define initializers to set up their initial state.
  • Be extended to expand their functionality beyond a default implementation.
  • Conform to protocols to provide standard functionality of a certain kind.

Classes have additional capabilities that structures don’t have:

  • Inheritance enables one class to inherit the characteristics of another.
  • Type casting enables you to check and interpret the type of a class instance at runtime.
  • Deinitializers enable an instance of a class to free up any resources it has assigned.
  • Reference counting allows more than one reference to a class instance.

How to choose between value types and reference types?

The most basic distinguishing feature of a value type is that copying — the effect of assignment, initialization, and argument passing — creates an independent instance with its own unique copy of its data.

Copying a reference, on the other hand, implicitly creates a shared instance. After a copy, two variables then refer to a single instance of the data, so modifying data in the second variable also affects the original.

One of the primary reasons to choose value types over reference types is the ability to more easily reason about your code. Importantly, you can safely pass copies of values across threads without synchronization.

When you’re working with Cocoa, many APIs expect subclasses of NSObject, so you have to use a class. For the other cases, here are some guidelines:

Use a value type when:

  • Comparing instance data with == makes sense
  • You want copies to have independent state
  • The data will be used in code across multiple threads

Use a reference type (e.g. use a class) when:

  • Comparing instance identity with === makes sense
  • You want to create shared, mutable state

Difference between @objc vs dynamic?

class Foo {
  func bar() -> Int {}
  @objc func bar1() -> Int { /* ... */ }
  @objc dynamic func bar2() -> Int { /* ... */ }
}

A function/variable declared as @objc is accessible from Objective-C, but Swift will continue to access it directly via static or virtual dispatch. This means if the function/variable is swizzled via the Objective-C framework, like what happens when using Key-Value Observing or the various Objective-C APIs to modify classes, calling the method from Swift and Objective-C will produce different results.

Using dynamic tells Swift to always refer to Objective-C dynamic dispatch. When the Swift function is called, it refers to the Objective-C runtime to dynamically dispatch the call. You might need this for KVO support or if you‘re doing method swizzling. The only way to do dynamic dispatch currently is through the Objective-C runtime, so you must add @objc if you use dynamic.

What is @escaping in Swift?

func foo(_ completion: @escaping () -> Void) {
  DispatchQueue.main.async {
    completion()
  }
}

@escaping is used to inform callers of a function that takes a closure that the closure might be stored or otherwise outlive the scope of the receiving function. This means that the caller must take precautions against retain cycles and memory leaks. It also tells the Swift compiler that this is intentional.

Difference between @objc and @nonobjc?

When you use the @objc(name) attribute on a Swift class, the class is made available in Objective-C without any namespacing. As a result, this attribute can also be useful when migrating an archivable Objective-C class to Swift. Because archived objects store the name of their class in the archive, you should use the @objc(name) attribute to specify the same name as your Objective-C class so that older archives can be unarchived by your new Swift class.

Conversely, Swift also provides the @nonobjc attribute, which makes a Swift declaration unavailable in Objective-C. You can use it to resolve circularity for bridging methods and to allow overloading of methods for classes imported by Objective-C. If an Objective-C method is overridden by a Swift method that cannot be represented in Objective-C, such as by specifying a parameter to be a variable, that method must be marked @nonobjc.