50 Essential IOS Interview Questions and Answers

Download as pdf or txt
Download as pdf or txt
You are on page 1of 125

Grokking Swift Updated for Xcode 15 & Swift 5

50 Interview
iOS

Questions & Answers


This resource is crafted to give you a taste of what
to expect in the Grokking the iOS Interview book.

If you find value in this free edition, we highly


recommend checking out the full book at
https://grokkingswift.gumroad.com/l/bsbwhj
1 - Describe the differences between structs and classes in
Swift. When would you choose one over the other?

Purpose: Evaluates the candidate's grasp of Swift's type system and their ability to

choose the appropriate data structure based on value semantics vs. reference

semantics, among other factors.

In Swift, both structs and classes are used to define properties and methods to add

functionality. However, there are key differences in how they are used, which stem from
Swift's value type (struct) versus reference type (class) distinction. Understanding these

differences is crucial for making informed decisions about when to use each in your iOS

applications.

Differences Between Structs and Classes

1. Value vs. Reference Semantics: The primary difference lies in how they are stored and

passed around. Structs are value types, meaning that when you assign a struct to a

variable or pass it to a function, a copy of the struct is created. Classes, on the other hand,

are reference types. When you assign a class instance to a variable or pass it to a function,

you are passing a reference to the same instance, not creating a new one.

2. Inheritance: Classes support inheritance, allowing one class to inherit the


characteristics of another. This is a cornerstone of object-oriented programming,

facilitating code reuse and polymorphism. Structs do not support inheritance.

3. Deinitializers: Classes can have deinitializers, which are called when an instance of the

class is about to be deallocated. Structs do not have deinitializers because their instances

are deallocated automatically once they are no longer needed, without the need for

additional cleanup.

GrokkingSwift 1
4. Memory Management: Classes use reference counting (ARC) for memory management.

Swift keeps track of how many references exist to each class instance and deallocates

instances when there are no more references. Structs, being value types, do not require

reference counting because each copy is independent, and Swift manages their memory

automatically.
5. Type Casting: With classes, you can use type casting to check and interpret the type of a

class instance at runtime. This is useful in conjunction with inheritance. Structs, lacking

inheritance, do not support this kind of runtime type checking.

6. Mutability: Classes are mutable by default. The properties of a class instance can be

changed even when the instance is assigned to a constant. If a struct instance is assigned

to a constant, none of its properties can be changed. There is one exception when you

mark the method as mutating. Swift creates a new instance of that type with the updated

values and assigns it back to the original variable. This is why you can't call a mutating

method on a constant (let) instance of a struct, as constants cannot be reassigned.

7. Automatic Memberwise Initializers: Structs automatically receive a memberwise

initializer if you don’t define any custom initializers. Memberwise initializer is not

available for classes, you have to explicitly define one.

8. Performance: Instances of classes are allocated on the heap while instances of structs

are typically allocated on the stack. Heap allocation is more complex and slower than
stack allocation due to the dynamic nature of heap memory management. In addition,

Swift uses an optimization called copy-on-write for structs. When a struct is copied, the

actual data is not immediately copied. Instead, both instances share the data until one of

them modifies it, at which point a real copy is made. This makes passing structs around
very efficient in terms of memory.

GrokkingSwift 2
Choosing Between Structs and Classes

Use a struct when:

The data encapsulated does not need to be inherited.


You want to ensure that the encapsulated values are copied rather than referenced.

You are encapsulating a few relatively simple data values.


An instance of the collection needs to be uniquely owned so that changes to data in

one instance don't affect another.

Choose a class when:

You need to create a complex hierarchical object model.

You need to share and modify instances between different parts of your app, reflecting
changes across the entire app.

The identity of the object matters, not just its current value.

Best Practices

Performance Considerations: Structs can offer better performance due to the simplicity

of value copying, especially for small data structures.


Safety and Predictability: Structs are generally safer and more predictable due to their

immutability and the fact that you work with copies of the data rather than shared
instances.

Complexity and Flexibility: Classes offer more flexibility and are suited for more
complex data models that require inheritance and type casting.

In practice, the decision to use structs or classes in Swift should be guided by the nature

of the data and the specific requirements of your application. Structs are generally
preferred for their performance benefits and safety, making them ideal for small data

GrokkingSwift 3
structures. Classes, with their reference semantics, inheritance capabilities, and
deinitializers, are better suited for more complex, shared, and mutable objects.

2 - Discuss the performance impact of inheritance and

protocol conformance in Swift.

Purpose: Tests candidate’s understanding of Swift's object-oriented and protocol-

oriented programming paradigms, focusing on how inheritance and protocol


conformance affect runtime performance and code structure.

Inheritance, a fundamental principle in object-oriented programming, allows a class to

inherit properties and behaviors from another class. This enables code reuse and
establishes a hierarchical relationship. Conversely, protocol conformance in Swift provides

a way to define a blueprint of methods and properties that a type must implement.
Comparing inheritance and protocol conformance in terms of runtime performance in
Swift involves understanding some core concepts and how they impact performance.

Method Dispatch

The performance impact of inheritance is primarily related to dynamic dispatch. When a

method is called on an instance of a class, the Swift runtime needs to determine which

implementation of the method to use based on the actual type of the instance. This
dynamic dispatch process involves looking up the method in the class hierarchy, which

takes time.

The more levels of inheritance a class has, the more expensive the method dispatch
becomes. Each additional level of inheritance adds a small overhead to the method lookup

GrokkingSwift 4
process. In scenarios where performance is critical, and there are deep inheritance
hierarchies, this can have a noticeable impact on execution time.

Protocols conformance, on the contrary, can use both static and dynamic dispatch.

Static dispatch happens when the compiler knows the exact type (e.g., when using
value types like structs) and optimizes calls, sometimes even inlining them (replacing

the method call with the method’s body), leading to faster execution.

Dynamic dispatch is used in polymorphic scenarios or when the exact type isn’t known
at compile-time. Swift uses a technique called witness tables to efficiently dispatch

protocol methods. Witness tables are lookup tables that map protocol requirements to

their corresponding implementations in the conforming type. When a method from a

protocol is called on an instance of a conforming type, the Swift runtime consults the
witness table to find the appropriate implementation. This allows different types

conforming to the same protocol to be used interchangeably while ensuring the correct

implementations are called.

Memory Footprint

Subclasses in an inheritance hierarchy can have a larger memory footprint if they inherit

many properties and method, which can impact performance, particularly in memory-
constrained environments.

Value types are often more lightweight compared to reference types. For a value type,

Swift allocates memory in the stack, which is more efficient compared to heap in terms of
memory allocation and deallocation.

Reference Semantics vs Value Semantics

GrokkingSwift 5
When you pass classes around, you’re passing references rather than copying the entire

object. Reference semantics avoid the overhead of copying. However, they can lead to
cache inefficiencies and require Swift to manage reference counting, which adds overhead.

Since reference types are managed on the heap, they can lead to memory leaks if strong

reference cycles are not properly handled. This occurs when two or more objects hold
strong references to each other, preventing their deallocation.

In addition, sharing reference type instances across multiple threads can lead to race

conditions and data corruption if not carefully managed. Ensuring thread safety requires

additional synchronization mechanisms, which can complicate the code and affect
performance.

In other ways, Swift uses an optimization technique called copy-on-write for value types.

When you copy a value type, Swift doesn't immediately make a full copy. Instead, it shares
the memory between the original and the copy. A full copy is only made if one of the

copies is modified. This makes using value types very efficient.

struct AnotherView: View {


var body: some View {
Text("Hello, SwiftUI!")
.font(.headline)
.foregroundColor(.blue)
.font(.body)
.foregroundColor(.red)
}
}

In practice, the choice between inheritance and protocol conformance often depends more

on the design patterns and architectural needs of your application, rather than on

GrokkingSwift 6
performance alone. Modern Swift development tends to favor protocol-oriented

programming, especially given Swift’s powerful protocol features and the performance

benefits they can offer. However, inheritance still has its place, especially when dealing
with UIKit and other frameworks that are heavily based on class hierarchies.

3 - Can delegation be implemented without a protocol? If

yes, then why do we need a protocol?

Purpose: Assess the candidate’s understanding of the delegation pattern, the

benefits of using protocol in implementing delegation, and how it encourages best


practices in software development.

Delegation can technically be implemented without using a protocol. However, using

protocols for delegation is considered a best practice and offers several benefits. Let's
explore this further.

In Swift, delegation is a design pattern that allows an object (the delegating object) to

pass some of its responsibilities or actions to another object (the delegate). Delegation

can be achieved without using a protocol by directly referencing the delegate object and
calling methods on it. Below is an example:

class DataSource {
var delegate: ViewController?

func fetchData() {
// Data fetching logic
delegate?.updateUIWithData()
}
}

GrokkingSwift 7
class ViewController {
let dataSource = DataSource()

func setupDataSource() {
dataSource.delegate = self
}

func updateUIWithData() {
// Update UI
}
}

Here, DataSource has a delegate property explicitly typed as ViewController. The


fetchData() method checks if the delegate is of type ViewController and, if so, calls the

updateUIWithData() method.

While this approach works, it has some drawbacks.

The delegate property is of a fixed type ViewController, which means it can only

delegates tasks to instances of ViewController. This tight coupling reduces the

flexibility and reusability of the code.


There is no clear contract or agreement between the delegating object and the

delegate regarding the methods that need to be implemented. As a result, there is no

compile-time safety. The delegating object doesn’t know if the delegate actually

implements the required methods.

Using protocols for delegation addresses the drawbacks mentioned above and provides

several advantages:

First, protocols provide a formal contract of methods, properties, and other


requirements that suit a particular task or piece of functionality. When you use

GrokkingSwift 8
protocols for delegation, it ensures that the delegate conforms to the expected

methods and properties, providing compile-time checks for method implementation.

This prevent runtime errors and enhances the overall safety of your code.

Second, protocols offer for a great deal of flexibility and code reuse. By defining a set
of behaviors that any class or struct can adopt, you can write more generic and

reusable code, allowing any object that conforms to the protocol to act as a delegate,

not just a specific class.

Third, protocols help in decoupling the delegate from the delegator. This means the

delegator doesn’t need to know the specifics about who is performing the delegated

task, as long as the delegate conforms to the protocol. This separation of concerns

makes your code more modular, easier to read, and maintain.


Finally, using protocols for delegation clearly documents the intended interaction

between components. Developers can easily see what methods and properties are

necessary for an object to be a delegate, improving the clarity and readability of the

code.

4 - How does Protocol-Oriented Programming in Swift differ


from Object-Oriented Programming?

Purpose: Assesses the candidate's understanding of Swift's programming paradigms

by comparing Protocol-Oriented Programming (POP) with traditional Object-

Oriented Programming (OOP), highlighting their conceptual and practical


differences in design and implementation.

Object-Oriented Programming (OOP) is a programming paradigm that organizes code into

objects, which are instances of classes. OOP focuses on creating classes that encapsulate

GrokkingSwift 9
data and behavior, and objects interact with each other through methods and properties.
The main concepts of OOP are encapsulation, inheritance, and polymorphism.

On the other hand, Protocol-Oriented Programming (POP) is a programming paradigm that

puts protocols at the center of the design. In Swift, protocols define a blueprint of
methods, properties, and requirements that conforming types must implement. POP

focuses on defining behavior through protocols and then implementing that behavior in

concrete types.

Here are the key differences between POP and OOP in Swift:

Emphasis on Protocols vs. Classes

In OOP, classes are the primary building blocks. Classes define the structure and

behavior of objects, and objects are instances of classes.

In POP, protocols are the primary building blocks. They define a set of requirements

and behaviors that conforming types must adhere to.

Composition vs. Inheritance

OOP relies heavily on inheritance, where subclasses inherit properties and behaviors

from a superclass. Inheritance creates a hierarchical relationship between classes

which can lead to tightly coupled code where changes to the superclass can have

unforeseen effects on subclasses.


POP encourages composition over inheritance. Protocols define a blueprint of methods,

properties, and other requirements, without implementing them. Types can adopt

multiple protocols and use protocol extensions to provide default implementations,

promoting code reuse and flexibility.

GrokkingSwift 10
Flexibility and Extensibility

OOP relies on inheritance for code reuse and polymorphism. This can lead to tight

coupling or situations where subclasses must inherit properties or methods they don't

need, potentially introducing errors.

POP provides more flexibility and extensibility. Protocols can be adopted by any type,

including structs, enums, and classes, allowing for more granular and modular design.

Default Implementation

In OOP, default implementations are provided through base classes or abstract classes,

which can be inherited by subclasses.

In POP, protocols can provide default implementations for methods and properties.
This allows for the definition of common behavior that conforming types can inherit or

override.

Value Types vs. Reference Type

OOP primarily focuses on reference types (classes). Objects are passed by reference,

which can lead to shared mutable state and potential side effects.

POP encourages the use of value types (structs and enums) whenever possible. Value

types are copied when assigned or passed around, providing better performance and

predictability.

In conclusion, both POP and OOP have their strengths and use cases. POP encourages a

more modular, flexible, and compositional design, while OOP provides a structured and

hierarchical approach to organizing code. Swift combines both paradigms, allowing

developers to choose the most appropriate approach based on the specific requirements
of their project.

GrokkingSwift 11
5 - Discuss the use of generics in Swift.

Purpose: Evaluates the candidate's ability to leverage generics for creating flexible

and reusable code components, a key for writing scalable and maintainable Swift

code.

Generics in Swift are a powerful feature that allows for the creation of flexible and

reusable code. By defining functions, types, and methods that can work with any type,

generics enable developers to write more abstract and less redundant code. Here are

several use cases of generics in Swift.

Generic Functions

Generics can be used to write functions that can operate on different types. Let’s consider

an example of a generic function that swap the values of two variables of any type.
Without generics, you would have to write separate functions for each type, such as

swapInts, swapStrings, swapDoubles, etc. With generics, you can write a single function

that swaps two values of any type, as long as they conform to a certain protocol.

func swapValues<T: Comparable>(_ a: inout T, _ b: inout T) {


let temp = a
a = b
b = temp
}

Now we can use the swapValues functions with different parameter types, as long as they

conform to Comparable protocol:

GrokkingSwift 12
// Test the generic function with different types
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

var someString = "Hello"


var anotherString = "World"
swapTwoValues(&someString, &anotherString)
print("someString is now \(someString), and anotherString is now \
(anotherString)")
// Prints "someString is now World, and anotherString is now Hello"

As you can see, the generic function swapValues works with both Int and String types. This

way, you can avoid writing multiple functions for each type, and make your code more

concise and generic.

Generic Types

A generic type can define one or more type parameters that act as placeholders for the

actual types it will work with. This allows the genetic type to be instantiated in a variety
of ways without requiring multiple definitions of the type. Let’s consider an example of a

struct Pair that takes two type parameters, T and U.

struct Pair<T, U> {


var first: T
var second: U

GrokkingSwift 13
init(first: T, second: U) {
self.first = first
self.second = second
}
}

This allows the Pair struct to work with any two types specified by T and U.

let intStringPair = Pair(first: 10, second: "Hello")


let boolDoublePair = Pair(first: true, second: 3.14)

Without using generics, we would need to define separate structs for each combination of

types we want to use in a pair. Here’s an example:

struct IntStringPair {
var first: Int
var second: String

init(first: Int, second: String) {


self.first = first
self.second = second
}
}

struct BoolDoublePair {
var first: Bool
var second: Double

init(first: Bool, second: Double) {


self.first = first
self.second = second
}
}

GrokkingSwift 14
To create instances of these pairs, we would do the following:

let intStringPair = IntStringPair(first: 10, second: "Hello")


let boolDoublePair = BoolDoublePair(first: true, second: 3.14)

This traditional approach demonstrates the lack of flexibility. If we want to use a different

combination of types, we need to define a new struct specifically for that combination. The

structs are tied to specific types, making it harder to reuse the code for different scenarios.

In contrast, the generic approach with Pair<T, U> provides a single, flexible struct that can
work with any two types. It eliminates code duplication, improves flexibility, and enhances

code reusability.

Type Constraints

Generics allow you to specify type constraints to restrict the types that can be used with a

generic type or function. For example:

func findIndex<T: Equatable>(of element: T, in array: [T]) -> Int? {


for (index, value) in array.enumerated() {
if value == element {
return index
}
}
return nil
}

In this example, the findIndex function has a type constraint T: Equatable, which requires

the type T to conform to the Equatable protocol. This ensures that the elements can be
compared for equality.

GrokkingSwift 15
If more than one requirement is needed for the type, you can use a protocol composition

to define type constraints.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {


// function body can now assume that 'someT' is an instance of
SomeClass
// and 'someU' conforms to SomeProtocol
}

You can also use where clauses to specify more complex constraints, including

relationships between type parameters like this:

func anotherFunction<T, U>(someT: T, someU: U) where T: SomeProtocol, U:


SomeProtocol, T.SomePropertyType == U.SomePropertyType {
// function body can now assume that 'someT' and 'someU' conform to
SomeProtocol
// and that they share the same associated type 'SomePropertyType'
}

Associated Types in Protocols

An associated type gives a protocol the ability to declare a placeholder name (an alias) for

a type that is used in its method signatures, property declarations, or other definitions.
The exact type to replace the placeholder is not specified until the protocol is adopted by

a concrete type. This provides flexibility and makes the protocol generic and reusable.

Consider a protocol that defines the requirements for a container type. A container should
be able to hold items, add an item, count its items, and provide access to an item at a

specific index. Here’s how you might define such a protocol using an associated type:

GrokkingSwift 16
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

In this protocol, Item is an associated type. It’s a placeholder for the type of items that the
container will hold. This allows any type that confirms to Container to specify the exact
type of items it contains.

Later on, a specific container, like an array, can conform to the Container protocol an
specify what type Item will be.

struct IntArrayContainer: Container {


var items = [Int]()

mutating func append(_ item: Int) {


items.append(item)
}

var count: Int {


return items.count
}

subscript(i: Int) -> Int {


return items[i]
}
}

GrokkingSwift 17
In IntArrayContainer, the associated type Item is specified to be an Int. Thus,
IntArrayContainer conforms to Container by implementing all its requirements with Item

replaced by Int.

Protocol Extensions with Generics

Generics can be used in combination with protocol extensions to add default

implementations for generic types. Here’s an example of adding a default implementation


for counting the occurrences of an element in a sequence.

extension Sequence where Element: Equatable {


func countOccurrences(of element: Element) -> Int {
return self.reduce(0) { count, currentElement in
return count + (currentElement == element ? 1 : 0)
}
}
}

Here, we extend the Sequence protocol with a generic constraint where Element:

Equatable. Now we can use countOccurrences method for any sequence whose elements
conform to the Equatable protocol.

let numbers = [1, 2, 3, 2, 4, 2, 5]


print(numbers.countOccurrences(of: 2)) // Output: 3

let letters = ["a", "b", "c", "a", "d", "a"]


print(letters.countOccurrences(of: "a")) // Output: 3

In summary, generic are immensely powerful in Swift, providing significant advantages in


terms of reusability, type safety, flexibility, and abstract design. They are widely used in the

GrokkingSwift 18
Swift standard library and in many third-party libraries. However, it’s important to note

that while generics offer powerful capabilities, overusing generics can sometimes lead to
increased complexity and reduced readability. It’s crucial to strike a balance and use

generics when they provide clear benefits to the codebase.

6 - What is the difference between Array and NSArray?

Purpose: Evaluates candidate’s understanding of the distinctions between Swift's

Array type and Objective-C's NSArray, highlighting differences in mutability, type


safety.

At their core, both Swift’s Array and Objective-C’s NSArray are used to store collections of

elements in a sequential manner. You can access elements by their index, or you can
iterate through the elements using a loop. Despite these similarities, there are key

differences between them, particularly around type safety, value vs. reference semantics,
which can significantly affect how you use them in different contexts in your iOS app
development.

Array is a generic collection type in Swift. It’s part of the Swift standard library and is a
struct, which means it’s a value type. Swift arrays are strongly typed, meaning the type of
the elements they can store is specified at the time of array creation. When you assign an

array to a new variable or pass it to a function, it’s copied. Any changes made to the copy
don’t affect the original array. If an array is declared with var, it’s mutable; if it’s declared

with let, it’s immutable.

NSArray , on the other hand, is a class from the Foundation framework of Objective-C. It’s
an object, which means it’s a reference type. NSArray can store any kind of object, but it

doesn’t enforce type safety like Swift arrays. It can even contain elements of different

GrokkingSwift 19
types. When you pass an NSArray around in your code, you’re passing a reference to the
same array. Changes made via any reference affect the same array. An NSArray is
immutable by its nature. Once you create it, you can’t add, remove, or replace it’s

elements. Instead, you gotta use NSMutableArray as a mutable version.

Swift provides automatic bridging between Array and NSArray. You can use them
interchangeably in many cases, but with some considerations due to copying.

7 - Explain the use of @escaping and @nonescaping


closures in Swift.

Purpose: Assesses the candidate's understanding of closure capture semantics,

including the lifecycle and memory management implications of @escaping


closures, and when to use each attribute.

In Swift, closures are self-contained blocks of functionality that can be passed around and

used in your code. Closures can capture and store references to any constants and
variables from the context in which they are defined. Swift treats closures as either

@escaping or @nonescaping, which tells the compiler about the closure's lifecycle and
how it captures variables.

@nonescaping Closures

By default, closures are nonescaping in Swift. A @nonescaping closure is one that is


called within the function it was passed into, meaning it doesn't escape the scope of the
function. Once the function returns, the closure cannot be called.

GrokkingSwift 20
When we use higher-order functions, such as map, filter, reduce, or sort, we often pass a
closure as an argument to transform or compare the elements of a collection. The closure

is a @nonescaping closure, because it is executed within the scope of the function, and
does not need to be stored or executed later.

@escaping Closures

An @escaping closure is passed as an argument to a function but is called after the


function returns. When you declare a closure as @escaping, you have to explicitly mark it

with the @escaping attribute. Escaping closures are necessary when the closure is stored
to be executed later, such as asynchronous callbacks or stored completion handlers.

Choosing Between @escaping and @nonescaping

@escaping closures can create strong reference cycles if they capture self or other

objects. To avoid this, we need to use weak or unowned references to break the cycle.
@nonescaping closures do not create strong reference cycles, because they are

deallocated when the function returns.


@escaping closures require explicit type annotations when used as function

parameters or return types. @nonescaping closures can be inferred by the compiler


without annotations.
@nonescaping closures allow the compiler to perform certain optimizations, such as

inlining or tail call elimination, that can improve the performance and memory usage
of the code. @escaping closures prevent these optimizations, because the compiler

cannot guarantee when or how they will be executed.

GrokkingSwift 21
8 - Explain the concept of optionals in Swift. How do they

help in making the code safer?

Purpose: Assesses the candidate's understanding of Swift's optionals, a


fundamental feature for managing nil values safely, highlighting their approach to

null safety and error handling.

An optional in Swift is a type that can hold either a value or nil to indicate that a value is
missing. They are similar to nullable references in other languages, but with a more

explicit and safer approach. Every type in Swift can be made optional by appending a ? to
its type. For example, Int? represents an optional integer which can be an integer or nil.

Why Optionals Make Code Safer?

Optionals prevent crashes and unexpected behavior that can occur when trying to access
non-existent values. Imagine a user profile app where you fetch the user's name from a

server. If the server returns an error or no data, accessing name without checking for nil
would lead to a crash. By using optionals, you force yourself to handle the possibility of a
missing value before using it, making your code more resilient.

In Swift, there are many techniques to unwrap optionals. Each technique has its use cases
and choosing the right one depends on the specific scenario and the level of safety you
need.

1. Optional Binding:

This is a safer way to unwrap optionals. Optional binding lets you check if an optional
contains a value, and if so, make that value available as a temporary constant or variable.

It's commonly used with if let or while let constructs. Optional binding is the preferred

GrokkingSwift 22
method to use when you're not sure if the optional contains a value as it prevents the risk
of runtime crashes due to nil values.

let optionalString: String? = "Hello"


if let actualString = optionalString {
print(actualString) // Safely unwrapped
}

2. Guard Statement

Guard statements are similar to optional binding but have the benefit of early exit,
meaning if the condition isn't met, the guard statement exits the current scope. It's useful
for unconditionally requiring a value.

func printString(_ optionalString: String?) {


guard let unwrappedString = optionalString else {
return
}
print(unwrappedString)
}

3. Force Unwrapping

Forced unwrapping uses the ! operator to directly access the value of an optional. It
should be used only when you're certain that the optional contains a non-nil value. If you

force unwrap a nil value, your app will crash.

let optionalString: String? = "Hello"


print(optionalString!) // Force unwrapping

GrokkingSwift 23
4. Optional Chaining:

This is a process that allows you to call properties, methods, and subscripts on an optional
that might currently be nil. If the optional is nil, the call simply returns nil instead of

crashing. It’s useful when you want to query multiple properties or methods down a chain
of potentially nil objects. Optional chaining is particularly useful in complex data models

where some parts of the chain might be nil and you want to handle these cases gracefully.

let count = optionalString?.count

5. Nil Coalescing Operator

The nil coalescing operator (??) unwraps an optional and returns the value inside if it
exists. If the optional is nil, it returns a default value.

let unwrappedString = optionalString ?? "Default Value"

6. Implicitly Unwrapped Optionals (IUOs)

These are optionals declared with an ! instead of a ?. They behave as if they are
automatically unwrapped. Use them when an optional will always have a value after its

initial set-up, but not necessarily when it's first declared.

var implicitlyUnwrappedString: String! = "Hello, world!"


let count = implicitlyUnwrappedString.count // No need for unwrapping

GrokkingSwift 24
The primary risk with IUOs is a runtime crash. Consider a scenario where you declare an

IUO as a property of a class but try to access it before it has been initialized with a value.

class MyViewController: UIViewController {


var myLabel: UILabel!

override func viewDidLoad() {


super.viewDidLoad()
// Assume we forget to initialize `myLabel` here
printLabel()
}

func printLabel() {
print(myLabel.text) // This will crash if myLabel is nil
}
}

This example demonstrates the risk associated with IUOs. They are convenient tool in

certain situations, but they can be dangerous if the guarantee of initialization is not met.

7. Pattern Matching

Pattern matching in a switch statement can be used to unwrap optionals. It's especially

useful in scenarios where you need to match against several potential states of an
optional.

Let's consider a function that takes an optional integer and performs different actions

based on whether the integer is nil, a specific value, or any other value.

func handleOptionalNumber(_ number: Int?) {


switch number {
case .none:
print("The number is nil.")

GrokkingSwift 25
case .some(42):
print("The number is 42. Do something")
case let .some(otherNumber):
print("The number is \(otherNumber), which is not 42.")
}
}
handleOptionalNumber(nil) // Output: The number is nil.
handleOptionalNumber(42) // Output: The number is 42. Do something
handleOptionalNumber(10) // Output: The number is 10, which is not 42.

Using pattern matching with optionals ensures all potential cases (including nil) are

handled, reducing the risk of unexpected crashes.

Best Practices

You should avoid forced unwrapping or implicitly unwrapped optionals except in cases

where you’re absolutely sure an optional won’t be nil. Although implicitly unwrapped
optionals are useful in certain contexts (like Interface Builder outlets), their overuse

can lead to unsafe code.


Optional binding using guard let is preferred for early exits. This makes your code
cleaner and avoids deep nesting.

If you need a default value for an optional if it is nil, consider the nil coalescing
operator (??).

Finally, when designing functions or models, think carefully about which values can be
nil and declare them as optionals accordingly.

9 - What is the difference between Static and Class variable


in Swift?

GrokkingSwift 26
Purpose: Evaluates the candidate’s knowledge of inheritance within the context of
Swift’s type property features, including their accessibility, override capabilities,

and use cases.

In Swift, static and class variables both belong to the type itself (like a class) rather than
to instances of the type. They share the same value across all instances of that type. Any

change made to these properties is reflected across all instances. The key difference is
that static variables cannot be overridden by subclasses while class properties can be.

Static Variable

class Game {
static var maxPlayers = 4

func printMaxPlayers() {
print("The maximum number of players is \(Game.maxPlayers).")
}
}

let chess = Game()


chess.printMaxPlayers() // Outputs: The maximum number of players is 4

Game.maxPlayers = 2

let checkers = Game()


checkers.printMaxPlayers() // Outputs: The maximum number of players is 2

In this example. maxPlayers is a static variable of the Game class. Changing its value via
Game.maxPlayers == 2 affects all instances of Game.

Class Variable

GrokkingSwift 27
Swift doesn’t directly support class variables as some other languages do. Instead, you

achieve similar functionality using class computed properties

class Game {
class var maxPlayers: Int {
return 4
}
}

class BoardGame: Game {


override class var maxPlayers: Int {
return 2
}
}

print(Game.maxPlayers) // Outputs: 4
print(BoardGame.maxPlayers) // Outputs: 2

In this example, BoardGame is a subclass of Game and overrides the maxPlayers


computed property. This allows different behavior for different subclasses, which isn’t

possible with static properties.

10 - Explain the use of lazy keyword in Swift

Purpose: Tests candidate’s understanding of Swift's lazy keyword, focusing on how

and why it's used to optimize performance by delaying the initialization of an


object until it's actually needed.

The lazy keyword is used to declare a property that will be initialized lazily, meaning its

initial value is not calculated until the first time it is accessed. Lazy properties are useful

GrokkingSwift 28
when the initial value of a property is computationally expensive or requires complex
setup, and you want to defer its initialization until it's actually needed.

Here are some key points about the lazy keyword in Swift:

When you declare a property as lazy, its initial value is not computed during the

initialization of the containing object. Instead, the initial value is computed and
assigned to the property only when it is first accessed.

Once a lazy property is initialized, its value is cached and subsequent accesses to the
property will return the cached value. The initialization code is executed only once,
regardless of how many times the property is accessed.

Lazy properties must be declared with the var keyword because their initial value is
not known at the time of object initialization. They cannot be declared with let since

constants must have a value assigned during initialization.


Lazy properties are beneficial when the initial value of a property is expensive to

compute or requires complex setup. By deferring the initialization until the property is
actually needed, you can avoid unnecessary computation and improve performance.
Lazy properties can access other instance properties and implicitly refer to self within

their initializer. This is because self is guaranteed to exist by the time the lazy property
is accessed.
You can't use a custom initializer for a lazy property; instead, you provide a closure that

initializes the property.

As stated in Apple’s documentation, lazy initialization is not thread-safe by default. If a

lazy property is accessed by multiple threads simultaneously, you must handle the
synchronization yourself.

GrokkingSwift 29
11 - What are the differences between static and dynamic
libraries in iOS?

Purpose: Assesses candidate’s understanding of the differences in linkage,

distribution, and impact on app performance and size between static and dynamic
libraries.

In iOS development, static libraries are compiled into the executable at compile-time.

Their code becomes part of the final executable. As a result, the size of the final

executable increases with the inclusion of static libraries. Nonetheless, iOS static libraries
cannot include images or assets, limiting them to code only. Using static libraries can

potentially improve performance as there’s no dynamic linking overhead at runtime.

However, updating a static library requires recompiling the app. Different versions of the

library cannot coexist in the same app.

Alternatively, dynamic libraries(also known as Shared Libraries) are loaded at runtime


when your app needs them. Using dynamic libraries reduces the size of the executable but

requires that the libraries be present on the system at runtime. Due to dynamic linking

and loading, there might be a slight performance cost. However, one of the advantages of

using dynamic linking is that they can be updated independently of the app.

The following table provides a comparison summary of both types of libraries.

Aspect Static Libraries Dynamic Libraries

Linking Time Compile-time Runtime

Increases (library code Decreases (library code not


Executable Size
included) included)

GrokkingSwift 30
Aspect Static Libraries Dynamic Libraries

Potentially faster (no runtime Slight overhead (runtime


Performance
linking) linking)

Can be updated
Versioning/Updating Requires recompiling the app
independently

Not shared, code copied into


Sharing Shared across multiple apps
each binary

Resource Bundling Only code, no resources Can include resources

Performance-critical, stable Shared components,


Typical Use Cases
code frequently updated

There’s no one-size-fits-all solution. Both static and dynamic libraries have their strengths

and weaknesses. When choosing an option between them, consider these factors:

App size and performance: If app size is crucial, dynamic libraries can help. For

performance-critical functions, static libraries might be better.


Dependencies: If your library has complex dependencies, a static library might be

simpler to manage. For independent updates, dynamic libraries shine.

Future updates: If you envision frequent updates to the library, dynamic libraries offer

greater flexibility.
Functionality integration: Tight coupling with your app’s code might favor static

libraries, while loosely coupled integrations can benefit from dynamic libraries.

GrokkingSwift 31
12 - What is the purpose of Core Location framework and
how to use it effectively?

Purpose: Evaluates candidate’s understanding of iOS's Core Location framework,

focusing on its role in providing location data and best practices for its effective

use in applications requiring geographical information.

Core Location framework is a tool provided by Apple for handling location-based services
and geographical information within iOS apps. It allows developers to access and use

location data to enhance the user experience in a variety of ways such as:

Providing the current geographical coordinates (latitude and longitude) of a user’s

device, and track changes in location, offering real-time updates as a user moves.

Tailoring content and services based on the user’s location. For example, showing
nearby restaurants or shops.

Providing turn-by-turn navigation or walking directions to a destination.

Creating virtual perimeters (geofences) around geographical locations and receive


notifications when a user’s device enters or exits these areas.

Triggering specific actions in an app when a user reaches a particular location.

Now that we’ve explored the features of Core Location, let’s delve into some key strategies

to effectively utilize this powerful tool in your next location-based app:

1. Choose the Right Accuracy Level

Core Location offers various accuracy levels and you need to dynamically adjust accuracy

based on the app’s current needs. Higher accuracy uses more battery, while lower accuracy

might not be precise enough for certain features.

GrokkingSwift 32
2. Respect User Privacy

Always be mindful of user privacy. Only request and use location data necessary for your

app’s functionalities. Ask for user consent before accessing location data, and clearly

communicate how it’s used. Transparency is key to building trust and avoiding user
frustration.

3. Handle User Location Updates

If you need to track location in the background, enable the Location updates background
mode in your app’s capabilities. Consider using significant location changes instead of

continuous updates. This saves battery and avoids unnecessary processing.

4. Optimize Resource Usage

Monitor battery consumption and network usage related to location services. Implement

mechanisms to minimize resource usage and ensure a smooth user experience.

5. Handle Errors

Deal with scenarios like location unavailable, authorization denied, or network issues

gracefully. Inform users and provide fallback options where necessary.

6. Test Thoroughly

Test your location-based features in various scenarios, including different accuracy levels,
network conditions, and user interactions. Ensure your app behaves as expected and

delivers a consistent, reliable experience.

GrokkingSwift 33
By following these strategies and best practices, you can utilize the power of Core
Location to build intuitive, engaging, and user-friendly location-based services that add

real value to your iOS apps.

13 - What are the possible ways to draw a circle on the UI


without using a UIBezierPath?

Purpose: Tests the candidate’s knowledge of iOS's graphics and drawing

capabilities, showcasing your familiarity with different frameworks and approaches


for creating custom UI elements.

There are several existing techniques to draw a circle without directly using UIBezierPath.

1. Using Core Graphics directly

Core Graphics, also known as Quartz 2D, is a low-level C-based API for drawing operations.
Since Core Graphics operates at a low level, it offers great performance for static drawing.

However, it requires more boilerplate code and understanding of the graphics context.

Moreover, drawing operations are CPU-intensive, so for complex or frequently updated

graphics, this method might impact performance.

You can draw a circle by overriding the draw() method of a UIView and using Core
Graphics functions. Here is an example.

class CircleView: UIView {


override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let lineWidth = 4.0
context.setStrokeColor(UIColor.green.cgColor)

GrokkingSwift 34
context.setLineWidth(lineWidth)

let center = CGPoint(x: bounds.midX, y: bounds.midY)


let radius = (min(rect.width, rect.height) / 2) - lineWidth
let drawRect = CGRect(
x: center.x - radius,
y: center.y - radius,
width: radius * 2,
height: radius * 2
)
context.addEllipse(in: drawRect)
context.drawPath(using: .fillStroke)
}
}

2. Using CAShapeLayer

CAShapeLayer is a Core Animation layer specialized in drawing vector-based shapes. It is

highly optimized for animation and rendering in the GPU. Generally, CAShapeLayer is more
performant than Core Graphics for animated drawings or when the shape needs to be

redrawn often, as the path is rasterized and cached by the GPU. This makes CAShapeLayer

and excellent choice for dynamic, interactive shapes. Here is how you can create a custom

circular view using CAShapeLayer.

class ShapeCircle: UIView {


override init(frame: CGRect) {
super.init(frame: frame)
setupCircleLayer()
}

required init?(coder: NSCoder) {


super.init(coder: coder)
setupCircleLayer()
}

GrokkingSwift 35
private func setupCircleLayer() {
let circleLayer = CAShapeLayer()
let circlePath = CGPath(ellipseIn: bounds, transform: nil)
circleLayer.path = circlePath
circleLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(circleLayer)
}
}

3. Rounding the corners of a UIView

This is a quick and straightforward method of drawing a circular view by setting the

cornerRadius property of a view’s layer to half of the view’s width or height. This approach
is less performant than Core Graphics or CAShapeLayer and it lacks flexibility for more

complex shapes or paths.

4. Using UIImageView with a Circle Image

This approach involves displaying a circular image within a UIImageView. It’s the simplest

way to add a circular element to the UI without any drawing code, using pre-rendered

images.

Using a UIImageView with a circular image is highly performant for static images, as the

image can be efficiently cached. The downside is the lack of flexibility and the additional
memory usage for image assets.

5. Drawing a circle in SwiftUI

If your project targeting iOS 13 and above, SwiftUI provides a Circle shape out of the box,

which you can use to draw a circle with minimal code. You can customize its appearance

GrokkingSwift 36
with various modifiers for size, color, border, and more. SwiftUI handles the rendering and

optimizations behind the scenes.

struct ContentView: View {


var body: some View {
Circle() // Creates a circle that fits the view's container.
.fill(Color.blue) // Fill color
.frame(width: 100, height: 100) // Size of the circle
.overlay(Circle().stroke(Color.red, lineWidth: 4)) // Border
color and width
.shadow(radius: 10) // Shadow
}
}

In summary, choosing the right method depends on the specific requirements of your

application, including whether the circle’s appearance is static or dynamic, the complexity

of the shapes involved, and the performance characteristics of the target devices.

14 - How do you implement adaptive UIs that support

different device sizes, orientations, and split view on iPad?

Purpose: Tests the candidate's proficiency in creating flexible and adaptive user
interfaces using Auto Layout, Size Classes, and UIKit, ensuring a seamless user

experience across all Apple devices.

There are various techniques and tools that you can leverage to implement adaptive UIs in

iOS.

GrokkingSwift 37
1. Use AutoLayout

AutoLayout allows you to build interfaces primarily using constraints, which define

relationships between UI elements and their parent views or other UI elements. These
constraints determine how views resize and move when the screen size or orientation

changes. On important thing to keep in mind is that you should always respect the safe

areas insets provided by system elements (notches, home indicators) to prevent content

from being overlapped.

In practice, even AutoLayout is officially supported by Apple, it is not as performant


as other layout frameworks. If you experience layout performance issues in UIKit

with extremely complex adaptive layouts, consider the frameworks listed at

https://github.com/layoutBox/LayoutFrameworkBenchmark.

2. Varying Layouts with Size Classes

You can utilize size classes to create distinct layouts for different screen sizes and

orientations. Size classes categorize the width and height of the screen into regular or

compact dimensions, allowing you to tailor your UI for variety of conditions. By overriding
traitCollectionDidChange or use the Vary For Traits feature in Interface Builder, you can

conditionally install/switch between layouts based on size classes.

3. Leverage Stack Views

UIStackView simplifies the process of designing adaptable UIs with Auto Layout. Stack

views manage a list of views in either a horizontal or vertical stack, automatically

adjusting their arrangement based on the stack view's properties. In addition, they handle
the spacing and alignment of their contained views, making it easier to manage layouts

that need to adapt to different screen sizes and orientations.

GrokkingSwift 38
4. Use Dynamic Type for Scalable Text

Adopting Dynamic Type will allow your app’s text to adjust size based on the user's

settings and ensures your app's typography is accessible and adaptive.

5. Handle Orientation Changes

Overriding viewWillTransition(to:with:) in your view controllers will allow you to respond

and make adjustments as needed when the device orientation changes.

6. Support Multitasking on iPad

For iPad apps, particularly those that support multitasking modes like Split View and Slide

Over, you can ensure your UI adapts to the available screen space by responding to

changes in the environments’s size class. Alternatively, UISplitViewController is your best


friend for master-detail interfaces, which automatically adjusts its presentation based on

the app's context and multitasking mode.

7. Test Across Devices and Configurations

Most importantly, testing! You can use either simulators or real devices to verify that your

adaptive UI works as expected on different screen sizes and orientations.

8. SwiftUI for Adaptive UIs

SwiftUI represents a paradigm shift in how we approach building user interfaces for iOS
and other Apple platforms. It allows developers to describe the UIs in a declarative way

with the primary use of stacks (HStack, VStack, ZStack). These stacks automatically adjust

their layout based on the available space, making it straightforward to build UIs that adapt

to various screen sizes and orientations. Ultimately, you can create a single codebase that

GrokkingSwift 39
works across all Apple platforms, adapting to each platform's unique characteristics and
user interface idioms.

15 - Explain the lifecycle of a UIViewController

Purpose: Evaluates candidate’s knowledge of the UIViewController lifecycle in iOS

development, understanding the various states a view controller goes through from

creation to destruction.

The lifecycle of a UIViewController is a series of events that occur from the creation to the
destruction of the view controller. Each method in the lifecycle provides a specific

opportunity to set up, configure, and tear down your view controller in response to

changes in its state.

Here’s the breakdown of the key states and their implications

1. Not yet created

This is the initial state, where the UIViewController instance hasn’t been allocated in

memory. This could be because:

You haven’t instantiated it yet.

It’s part of a storyboard scene that hasn’t been loaded.


It’s managed by a container view controller and hasn’t been added as a child yet.

2. Initialized but not loaded

The UIViewController is initialized. This can happen programmatically or when loaded

from a storyboard. The controller exists, but its view isn’t created or loaded until the

GrokkingSwift 40
associated screen appears. loadView() will be called when the controller’s view needs to
be created. You rarely call this method directly.

3. Loaded

The view controller has loaded its view into memory. It’s a good place to do initial setup,
such as setting initial data, adjusting layout constraints, and setting up subviews.

viewDidLoad() is called at this state.

4. Appearing and Layout

viewWillAppear() is called before the view is added to the view hierarchy, indicating

the controller’s view is about to be displayed on screen. This is a good opportunity to

update UI elements dynamically based on current context or perform final preparations


before visibility.

viewWillLayoutSubviews() is called to notify the view controller that its view is about
to layout its subviews. This is a good place to make changes before the layout occurs.

viewDidLayoutSubviews() is called to notify the view controller that its view has just

laid out its subviews. This is ideal for making changes after the layout.

viewDidAppear() is called when the controller’s view is fully visible on screen. This is

the ideal time to start animations, fetch data, or interact with the user through the UI.

5. Disappearing

viewWillDisappear() is called, indicating the disappearance of the controller’s view.


This method is called when the view is about to be removed from the view hierarchy

GrokkingSwift 41
or the view is about to be hidden. This is where you should stop animations, cleaning

up resources in response to the viewDidAppear() method call.

viewDidDisappear() is called, confirming the controller’s view is no longer visible on


screen. This is where you release the unneeded resources when the view is not part of

the window.

6. Unloading and Deinitialization

Usually under low-memory conditions, the view controller’s view will be released from

memory. In such cases, didReceiveMemoryWarning() is called and you can dispose

resources that are not in use or clear caches to free up memory.

Finally, the view controller is deinitialized and removed from memory when it is no longer

used or referenced. Usually, manual cleanup isn't necessary when instances are
deallocated. However, if you’re woking with custom resources, such as opening a file and

writing some data to it, additional cleanup may be needed.

16 - When configuring a UITableView, which


UIViewController lifecycle method is suitable for setting the
dataSource and reloading the data?

Purpose: Evaluates candidate’s understanding of the optimal points within a

UIViewController's lifecycle for configuring UITableView components, ensuring


efficient data display and interaction.

GrokkingSwift 42
The dataSource of a UITableView should ideally be set early in the view controller’s

lifecycle. The most suitable place to do this is in the viewDidLoad() method. At this point,
the view hierarchy is loaded into memory, but the layout has not been done yet. This is a

perfect time for initial setup tasks. Moreover, viewDidLoad() is called only once during the

lifecycle of the view controller, setting the dataSource here will avoid unnecessary

repetitions. Doing it in methods likes viewWillAppear() or viewDidAppear() would set it


multiple times during the lifecycle, which is inefficient.

class MyViewController: UIViewController, UITableViewDataSource {


@IBOutlet weak var tableView: UITableView!

override func viewDidLoad() {


super.viewDidLoad()
tableView.dataSource = self
// Additional setup...
}
}

When to reload a UITableView depends on the context of your data:

If your data is static or preloaded, you can reload the table view in viewDidLoad()

If you’re fetching data asynchronously, then you should reload the table view after the

data has been fetched and is ready. This might be done, for example, in a completion
handler of a network request.

If your data changes every time the view appears, or depends on interactions in other

parts of your app, you should reload the table in viewWillAppear().

You can reload a UITableView either completely using reloadData() or partially using

methods like reloadSections(:with:) or reloadRows(at:with:). However, reloading a


UITableView should always be done on the main thread because UIKit and most of Apple’s

GrokkingSwift 43
UI frameworks are not thread-safe. Performing UI updates on a background thread can

cause crashes and unpredictable behavior.

To reload the entire table view:

DispatchQueue.main.async {
self.tableView.reloadData()
}

Or partially reload specific sections or rows:

let indexPathsToReload = [
IndexPath(row: 0, section: 0),
IndexPath(row: 1, section: 0)
]
DispatchQueue.main.async {
self.tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}

17 - What are the differences between clipsToBounds and


masksToBounds?

Purpose: Assesses candidate’s understanding of UI components' rendering behavior


in iOS development, particularly how they handle content clipping and layer

masking.

Both clipsToBounds in UIKit and masksToBounds in Core Animation properties determine

how subviews are displayed within their superviews, but they belong to different parts of

GrokkingSwift 44
the iOS rendering system.

clipsToBounds belongs to UIView and it determines whether the view’s content (subviews)

are clipped to the view’s bounds. The default value of clipsToBounds is false, meaning

subviews can be rendered outside the bounds of their superview. When set to true, any

portion of a subview that extends beyond the bounds of its superview will be clipped and
not rendered. It acts like a physical frame, cutting off anything overflowing. For instance,

it’s often used with UIImageView to create rounded corners.

let imageView = UIImageView()


imageView.clipsToBounds = true
imageView.layer.cornerRadius = 10

In this example, clipsToBounds ensures that the image is clipped to fit within the rounded

corners defined by cornerRadius.

masksToBounds, on the other hand, belongs to CALayer. It’s similar to clipToBounds in that

it determines whether an element’s content is clipped to the bounds of its layer. Thus, it’s
often used in layer-based custom drawing and animations. For instance, when adding

shadow or border effects to layers.

let layer = CALayer()


layer.masksToBounds = true
layer.cornerRadius = 10

These properties can be used together, but carefully. If you set both clipsToBounds and

masksToBounds to true, the clipping will happen first, followed by masking. This can lead

to unexpected results if masks rely on content that has already been clipped.

GrokkingSwift 45
In terms of performance, clipsToBounds doesn’t significantly impact memory usage
compared to masksToBounds, as it simply cuts off overflowing content without any

masking calculations. By contrast, applying complex masks or shapes can be

computationally expensive. sometimes lead to off-screen rendering, which can be a


performance concern, especially with animations or scrolling.

Common use cases

When you need a UIView (like UIImageView, UIButton, etc.) to have rounded corners,
you often set layer.cornerRadius and enable clipsToBounds. This ensure the view’s

content respects the rounded boundaries.

masksToBounds is commonly used in conjunction with shadows and borders.

18 - What are the differences between Frame and Bound?

Purpose: Evaluates candidate’s understanding of UIKit's coordinate system.

The frame of a view is a rectangle, defined by a size and a position, that determines the

view’s location and size in its superview’s coordinate system. It includes the origin (a

CGPoint) and size (a CGSize).

Changing the origin of frame moves the view within it’s superview and changing the size
of frame resizes the view. The below example illustrates a view’s frame. This view has a

frame of (50, 100, 240, 164), meaning it is positioned 50 points and 100 points from the

left and top edges, respectively. And the view’s size is 240 points in width, 164 points in
height.

GrokkingSwift 46
Bounds represents the view’s intrinsic size and origin in its own coordinate system. Just

like frame, bounds includes an origin and size.

Changing the origin of bounds shifts the starting point of the view’s internal coordinate

system while changing the size can either resize, zoom or scale the view’s content.

Here’s an example to illustrate a view’s bounds:

GrokkingSwift 47
Practical Tips

When laying out subviews within a view, you often use the view’s bounds, as it gives

you a coordinate system relative to the view itself, not its superview.

When positioning a view within its superview, you adjust the view’s frame.
In custom drawing or when working with transformations (like rotation or scaling),

bounds is usually more relevant because it deals with the view’s internal coordinate

space.

19 - What are the differences between setNeedsLayout()


and layoutIfNeeded()?

GrokkingSwift 48
Purpose: Tests candidate’s understanding of the UIView layout process in iOS,

specifically distinguishing between the deferred layout updates and the immediate
layout adjustments triggered.

setNeedsLayout() marks a view as needing layout updates without immediately triggering


those updates. When you call setNeedsLayout(), you are signaling to the system that the

view’s layout properties (e.g., frame, bounds, constraints) have changed and that the

layout needs to be recalculated. The actual layout update will be performed


asynchronously, typically on the next screen update.

Calling setNeedsLayout() multiple times in quick succession will not trigger multiple

layout updates. The system optimizes it to perform the layout update only once.

Here's an example. Imagine you’re developing a feature where you have a container view

with several subviews. You want to change the frame of multiple subviews based on user

interaction, but you prefer to perform all the layout updates at once for efficiency.

class MyViewController: UIViewController {


var view1: UIView!
var view2: UIView!

func updateUserInterface() {
// Modify the frames of subviews
view1.frame = ... // new frame for view1
view2.frame = ... // new frame for view2

// Schedule a layout update in the next update cycle


self.view.setNeedsLayout()
}
}

GrokkingSwift 49
layoutIfNeeded() forces the view to update its layout immediately based on the current

constraints and layout properties. It performs the layout update synchronously, meaning

that the layout will be updated before the next line of code is executed. You typically call
layoutIfNeeded() when you need to ensure that the layout is up to date before performing

certain operations or when you want to animate layout changes.

Let’s examine an example where you want to update layout when a view expands its size

and you want the size change to be animated. Here’s how you can achieve it:

class MyViewController: UIViewController {


var expandableView: UIView!

func animateViewExpansion() {
// Change the frame of the view (without animation)
expandableView.frame = ... // new, larger frame

// Animate the layout change


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

In this example, the frame of expandableView is first adjusted. Then, within the

UIView.animate block, layoutIfNeeded() is called. This forces any pending layout changes

to be committed right away, and since it's inside an animation block, the frame change is
animated.

layoutIfNeeded() will only perform a layout pass if there are pending layout updates for

the view or its subviews. Without prior changes to the layout(either setNeedsLayout() is

GrokkingSwift 50
not called, or no other layout-affecting actions), calling layoutIfNeeded() will have no

effect.

What is a layout pass and what are other layout-affecting actions? You might ask.

A layout pass (or its full name - The Deferred Layout Pass) in iOS development is the

process where the system calculates the size and position of all the views in the view

hierarchy. This process involves measuring and arranging the views according to their
constraints (if using Auto Layout) or their frames and bounds. A layout pass involves two

stages:

The measurement stage where each view calculates its size based on its content,

constraints, and the bounds of its parent view.

The arrangement stage where each view is positioned within its parent view’s
coordinate system after sizes are calculated.

The deferred layout pass can be triggered by various events, such as:

Changes to the view hierarchy (adding or removing views).

Changes to constraints in a view using Auto Layout.

Explicit requests for layout updates using methods like setNeedLayouts() or

layoutIfNeeded()
Changes to the size of a view’s bounds (like when rotating the device, changing the

size of a split view, etc.)

In summary, setNeedsLayout() is used to mark a view's layout as needing an update, which

will be performed asynchronously, while layoutIfNeeded() forces an immediate layout

update. Understanding the difference between these methods helps in properly managing
and optimizing the layout updates in your iOS app.

GrokkingSwift 51
20 - How does Auto Layout work on iOS?

Purpose: Evaluates candidate’s understanding of Auto Layout, including how it

dynamically calculates the size and position of views based on constraints, to

support different screen sizes and orientations in iOS.

Auto Layout is a constraint-based layout system, which means rather than setting the

exact position and size for each view, you define rules (constraints) for the layout.
Common types of constraints include:

Size constraints (width and height)

Positional constraints (leading, trailing, top, bottom)

Relative constraints (aligning to another view)

Auto Layout employs a constraint solver to calculate the size and placement of each

element according to the provided constraints. Many views have intrinsic content size (like
a label with text). Auto Layout also uses this size as part of the constraints solving process.

During a layout pass, the system sets the frame for each view according to the solved

constraints.

When working with Auto Layout, ambiguous constraints are a common issue. This typically
happens when there aren’t enough constraints to fully describe a view’s size and position.

For example, if a view has a constraint for its top position but no constraints for its height,

width, or bottom position, the system can’t determine where exactly the view should be.

Another issue is the constraints conflict with each other, providing contradictory
instructions about a view’s size or position. These issues lead to unpredictable UI at

runtime.

GrokkingSwift 52
To resolve ambiguity, you need to add enough constraints to unambiguously define the
size and position of all views. Moreover, not all constraints are equal. In some cases you

will have to set priorities to indicate which constraints should be considered more

important.

21 - Explain how SwiftUI’s environment works

Purpose: Accesses the candidate's knowledge of SwiftUI's environment system, a


mechanism for passing data and managing dependencies down the view hierarchy.

SwiftUI’s environment is a collection of values managed by the system that can be

accessed by any view with the view hierarchy. This is a key-value store that can hold

various types of data such as system settings, theme information, or app-specific data.

Using SwiftUI’s environment allows you to pass data and dependencies through the view

hierarchy without explicitly passing them as parameters to each view, or using other forms
of data propagation like delegation or closures. The environment is inherited by child

views from their parent views, creating a hierarchical data flow. Views can access and

modify the environment as needed, and any changes to the environment are propagated

down the view hierarchy. You need both @Environement and @EnvironmentObject to
achieve such goal.

@Environment

The @Environment property wrapper allows individual views to read specific environment
values that are provided by the system or custom values that you define. Here’s an

example of using @Environment to access the current color scheme:

GrokkingSwift 53
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme

var body: some View {


Text("Current color scheme: \(colorScheme == .dark ? "Dark" :
"Light")")
}

In this example, the @Environment(.colorScheme) property wrapper retrieves the current

color scheme from the environment. The view can then use this value to conditionally

modify its appearance or behavior based on the color scheme.

If you wish to add your custom values to the environment, you need to create an

EnvironmentKey and then extend EnvironmentValues with your own environment


property. For example, we’ll create a custom environment key called FontSizeKey to

control the font size of the text throughout the app.

struct FontSizeKey: EnvironmentKey {


static let defaultValue: CGFloat = 24
}

extension EnvironmentValues {
var fontSize: CGFloat {
get { self[FontSizeKey.self] }
set { self[FontSizeKey.self] = newValue }
}
}

GrokkingSwift 54
And use it in a view hierarchy like so:

struct ContentView: View {


@Environment(\.defaultFontSize) private var defaultFontSize

var body: some View {


Text("Text to apply the default font size")
.font(.system(size: defaultFontSize))
}
}

@EnvironmentObject

The @EnvironmentObject property wrapper is used to share data objects instance across

multiple views in the hierarchy. An environment object must conform to the

ObservableObject protocol.

Here's an example of using @EnvironmentObject to share an instance of a view model:

First, we define a view model that conforms to ObservableObject.

class MyViewModel: ObservableObject {


@Published var count = 0
}

This view model simply holds a count value. Like other observable objects, any changes to

the properties within the object marked with @Published will trigger a view update. This

reactivity is central to SwiftUI’s data-driven UI approach.

GrokkingSwift 55
Next, suppose we will inject this view model from a ParentView, then access and modify it

within a ChildView. Here’s how our ParentView looks like:

struct ParentView: View {


@StateObject private var viewModel = MyViewModel()

var body: some View {


ChildView()
.environmentObject(viewModel)
}
}

The ParentView creates an instance of MyViewModel using @StateObject and injects it

into the environment using .environment(viewModel). The @StateObject wrapper ensures

that the observable object is only created once during the lifetime of the view, even if the
view is recomposed multiple times due to state changes elsewhere in the UI.

Now let’s define our ChildView:

struct ChildView: View {


@EnvironmentObject var viewModel: MyViewModel

var body: some View {


VStack {
Text("Count: \(viewModel.count)")
Button("Increment") {
viewModel.count += 1
}
}
}
}

GrokkingSwift 56
This ChildView can access the shared instance of MyViewModel using the syntax of

@EnvironmentObject var viewModel: MyViewModel. Any changes made to the viewModel

will be reflected in the ParentView and any other views that access the same
@EnvironmentObject.

There is one important note here, though. @EnvironmentObject assumes that the
required object is available in the environment. If the object is not found, a runtime error

will occur. So, you need to ensure that the object is properly injected into the environment

at a higher level in the view hierarchy.

22 - What strategies would you employ to optimize the


performance of a SwiftUI app?

Purpose: Evaluates the candidate's awareness of performance considerations in


SwiftUI apps and their ability to apply optimization techniques to ensure smooth

and responsive UIs.

Optimizing the performance of a SwiftUI app involves understanding and addressing

potential bottlenecks in view rendering, data processing, and memory management. Here

are strategies to enhance app performance:

Streamline the View Hierarchy

Reduce the complexity of your views by breaking them down into smaller, reusable

components. SwiftUI is efficient at rendering small, focused views. You should use
conditions to render views only when necessary, avoiding the overhead of rendering

views that are not visible or needed.

GrokkingSwift 57
Minimize the use of AnyView. While it is useful for type-erasing and can help with

complex view compositions, it comes with a performance cost. Overusing AnyView can

lead to slower view updates and increased memory usage. Instead, you would aim to

use concrete view types whenever possible and only resort to AnyView when
absolutely necessary.

Lazy Loading and Caching

Implementing lazy loading techniques will improve your app’s responsiveness. This
involves loading data and resources only when they are needed, rather than upfront.

Additionally, you should cache frequently accessed data to reduce the need for

repeated network requests or expensive computations.

When working with large datasets in a list or grid format, utilize LazyVStack,
LazyHStack, LazyVGrid, LazyHGrid to load views only as they come into view.

Efficient Data Flow

SwiftUI relies heavily on data flow and bindings. To optimize performance, ensure that
the data flow is efficient and avoid unnecessary updates. For example, you would use
@State for local view state, @ObservedObject for shared data across view, @Binding

for passing data between views. Proper use of these property wrappers helps minimize
unnecessary view redraws.

Avoid complex computations in views

Complex computations or data processing should be moved out of the view hierarchy
and into separate models or view models. This allows the views to focus on rendering

and keeps the UI responsive. You could use @StateObject to manage the state of these
models and ensure they are created only once per view instance.

GrokkingSwift 58
Minimize Use of Complex Animations

While animations enhance user experience, they can also impact performance.
Consider disabling or simplifying animations for lower-end devices or when battery
saving modes are active.

Asynchronous Operations

Time-consuming tasks like network requests or file I/O should be performed


asynchronously. This prevents blocking the main thread and keeps the UI responsive

while the tasks are being executed in the background.

Optimize Images and Assets

Images can significantly impact app performance if not optimized properly. Before

including them in your app, make sure you have compressed the images to the
smallest size necessary. Using the appropriate image resolutions for different device
sizes is also important. You can consider use a single vector image (.pdf) instead of

multiple .png or .jpg files as the vector image will automatically scale for different
resolutions.

Profiling and measuring

Regularly profile your app with Instruments to identify performance bottlenecks,


memory leaks, and inefficient code paths.

Test your app’s performance across different devices and iOS versions to ensure
consistent performance, particularly on older hardware.

GrokkingSwift 59
23 - Why does SwiftUI use structs for views?

Purpose: Assesses the candidate's understanding of SwiftUI's design principles,

specifically how its choice of structs for views contributes to efficient memory
management, immutability, and the reactive programming model.

SwiftUI uses structs for views because of several key reasons that align with the goals and
principles of the framework:

Immutability

SwiftUI uses structs for views primarily because structs are immutable by default. This
immutability ensures that each view instance is unique and independent. Furthermore, it
prevents unintended sharing of state between views, reducing the chances of unexpected

side effects and making the code more predictable.

Performance

Structs are allocated on the stack, which is generally faster than heap allocation used for

classes (reference types). Since views in SwiftUI are frequently created, updated, and
discarded, using structs helps optimize memory usage and performance.

Memory Management Efficiency

Unlike classes, where deallocation timing can be less predictable due to the complexities
of reference counting and potential retain cycles, structs are deallocated as soon as they
go out of scope. This predictability ensures that memory used by views is instantly freed

once they are no longer needed, without the complexities of tracking ownership and
references.

GrokkingSwift 60
Finally, since structs are value types and do not share memory the way reference types do,
they naturally avoid many common concurrency problems related to shared mutable state.

24 - Does the order of SwiftUI modifiers matter?

Purpose: Assesses the candidate's understanding of how SwiftUI modifiers affect


the behavior and appearance of views.

The order of SwiftUI modifiers matters significantly, as it can affect the final appearance
and behavior of your views.

When you stack modifiers on a view, the are applied in the order they appear in the view’s
modifier chain. The first modifier in the sequence is applied to the original view, the
second modifier is applied to the result of the first modification, and so on. This sequential

application can lead to different outcomes depending on the order in which you arrange
the modifiers.

For example, if you apply .padding() before .frame(), the padding is included within the
frame size calculation. Conversely, if .frame() is applied first, the specified frame size is set
before padding is added outside of it.

Text("Modifiers") // The original view


.background(.green)

Text("Modifiers")
.padding() // First, padding is applied.
.frame(width: 120, height: 100) // Then, the frame is set, including
the padding in its size.
.background(.green)

Text("Modifiers")

GrokkingSwift 61
.frame(width: 120, height: 100) // First, the frame is set.
.padding() // Then, padding is added outside the frame, increasing the
total size.
.background(.green)

The above code will result the text in different layouts and positions

The order of modifiers can also impact the rendering and performance of a view. Certain
modifiers such as opacity(), blur(), or shadow() can be computationally expensive and
affect the rendering performance. Applying these modifiers earlier in the modifier chain

can result in unnecessary rendering overhead, especially if the view is frequently updated.
It’s generally recommended to apply performance-intensive modifiers later in the chain,
after other layout and styling modifiers have been applied.

GrokkingSwift 62
It’s also important to note that when multiple modifiers of the same type are applied to a

view, only the first modifier in the chain is applied. Subsequent modifier of the same type
are ignored. This behavior ensures that the most specific modifier takes precedence and
avoids conflicts or unexpected results. To illustrate this, consider the following example:

Text("Hello, SwiftUI!")
.font(.headline)
.foregroundColor(.blue)
.font(.body)
.foregroundColor(.red)

Here, the text will have a headline font size and a blue foreground color. The second
font(.body) and foregroundColor(.red) modifiers are ignored because they are of the same

type as the previous modifiers in the chain.

25 - Explain how ARC works in Swift. What are the common

pitfalls you might encounter while using ARC?

Purpose: Tests the candidate's understanding of memory management in Swift,


particularly Automatic Reference Counting (ARC), and challenges such as retain
cycles and memory leaks.

Automatic Reference Counting (ARC) in Swift is a memory management mechanism that


automatically manages the memory usage of your application. It works by keeping track of

and managing the memory usage of objects by counting the number of references to each
object. When an object's reference count drops to zero, meaning no part of your code has a
reference to it, ARC automatically frees up the memory used by that object. This system

GrokkingSwift 63
helps in reducing memory leaks and managing memory more efficiently without the need
for manual memory management.

How ARC Works

ARC keeps track of how many references there are to each object. When you create a
reference to an object, ARC increments its reference count. When the reference is no
longer needed, ARC decrements the reference count.

When an object’s reference count becomes zero, ARC deallocates the object, freeing up the
memory space it occupied. This process is automatic, relieving developers from having to

manually manage the object’s lifecycle.

Common Pitfalls

Despite its efficiency in managing memory, using ARC comes with pitfalls that you need to

be aware of:

1. Strong Reference Cycles

One of the most common issues with ARC is the creation of strong reference cycles. This
occurs when two object instances hold strong references to each other, preventing ARC
from deallocating them, as their reference counts never reach zero. To resolve this, you can

use weak or unowned references when defining properties or variables that may create a
cycle.

2. Unowned References and Dangling Pointers

Unlike weak references, unowned references are expected to always have a value.

However, if the object to which an unowned reference points is deallocated, trying to

GrokkingSwift 64
access that reference can lead to a runtime crash. It’s important to ensure that unowned

references always point to an allocated object.

3. Memory Leaks and Overhead

While ARC significantly reduces the chances of memory leaks, it's not foolproof. Improper
management of closures, strong reference cycles, or global strong references can lead to
memory leaks. Additionally, the overhead of keeping track of reference counts can impact

performance in very object-heavy applications, though this is rarely a significant concern


in practice.

4. Capture Lists in Closures

Closures capture variables and constants from their surrounding context, which can lead

to strong reference cycles if not handled carefully. Using capture lists in closures, where
you specify weak or unowned references to captured instances, can help prevent these
cycles.

Understanding and navigating these pitfalls are crucial for efficient iOS app development
under ARC. You must be mindful of their references, choose weak or unowned

appropriately, and be aware of the contexts in which strong reference cycles can occur to
ensure optimal memory management and application performance.

26 - Why are IBOutlets weak by default? What happens

when we make them strong?

Purpose: Accesses the candidate’s understanding of memory management in iOS,


particularly in relation to Interface Builder and the use of IBOutlets. It also touches

GrokkingSwift 65
on key concepts such as reference cycles, memory leaks, and the ownership
relationship between views and their view controllers.

IBOutlets provide a means to reference interface elements that are instantiated from your
app’s storyboard or XIB files. The reason why IBOutlets are weak by default is to primarily

avoid retain cycles.

Retain cycles occurs when two or more objects hold strong references to each other,
preventing them from being deallocated and leading to memory leaks. In the context of

IBOutlets, the view controller typically holds a strong reference to its view, and if the
IBOutlets within this view also holds strong references back to the view controller ( or to
the view that the controller already strongly references), a retain cycle could be created.

By defaulting IBOutlets to weak, iOS ensures that views can be released from memory
when they are no longer in use. A weak reference does not increase the retain count of an

object, thus when the view controller's view is dismissed, it and its subviews are
deallocated, preventing memory leaks.

What Happens When IBOutlets are Made Strong?

As mentioned, strong IBOutlets can create retain cycles if the views they reference also
hold a reference back to the view controller (directly or indirectly), leading to memory
leaks. Consequently, even if the view controller is no longer needed and should be

deallocated, it will persist in memory due to the retain cycle. This can negatively impact
the performance and stability of your app.

In some specific scenarios, such as where you programmatically remove a view from its
superview and intend to re-add it later, you can make an IBOutlet a strong reference.
When doing so, the view controller will keep a strong hold on the UI element. This means

GrokkingSwift 66
the UI element is guaranteed to exist as long as the object with the strong IBOutlet is

itself in memory. With strong IBOutlets, you take on more responsibility for memory
management. To avoid leaks, you’ll usually need to explicitly set these strong IBOutlets to

nil during deallocation.

In practice, it’s the safest default practice to prefer weak for IBOutlets, unless you
understand the memory implications and have a very specific reason for a strong

reference.

27 - Describe how you would implement a feature that

requires real-time data updates from a server (e.g., a stock


market app).

Purpose: This question tests the candidate's ability to design and implement real-

time data communication in an iOS app, covering practical considerations such as


using WebSockets, handling network interruptions, and updating the UI efficiently.

Here’s a structured approach to tackle this challenge:

1. Choosing the Right Architecture

For real-time data, traditional HTTP requests (RESTful services) might not be efficient

due to the constant polling required. Instead, WebSockets provide a full-duplex


communication channel over a single, long-lived connection, allowing the server to
push updates to clients in real-time.

Depending on the backend technology, alternatives or third-party real-time data


services (e.g., Firebase Realtime Database, Pusher) might also be suitable.

GrokkingSwift 67
2. Network Efficiency

Only send updates for data that has changed, rather than sending the entire dataset
with each update.

Depending on the use case, consider throttling updates on the server side to prevent
overwhelming the client with too much data at once.

3. Error Handling

Implement automatic reconnection logic to handle intermittent connection losses


without flooding the server with reconnection attempts.
After a reconnection, ensure data consistency by fetching the latest snapshot of the

relevant data to catch up with any missed updates.

4. User Experience

Use placeholders or loading indicators for data that is expected but not yet received to

keep the user informed that updates are pending.


In case of connection loss, inform users about the connection status (e.g., "Attempting
to reconnect…") and provide visual feedback when data is updated or when an error

occurs.
Cache data locally to provide some functionality even when the device is offline. When

the connection is reestablished, update the local cache with the latest data.

5. Testing and Optimization

Stress test your implementation under various network conditions to ensure stability
and performance.

GrokkingSwift 68
Use profiling tools to monitor the app's performance and memory usage, optimizing as
necessary.

28 - Describe the types of sessions that the URLSession

supports.

Purpose: Evaluates candidate’s knowledge of network communication in iOS by


examining their familiarity with the URLSession class, including the various

session configuration types it supports for handling HTTP requests.

URLSession is a powerful tool for handling HTTP requests and managing network tasks. It

provides various types of sessions to accommodate different networking needs.

1. Shared Session

This is the pre-configured singleton instance of URLSession. It’s a convenient way to

quickly access a shared session without the need to create and manage a separate session
instance.

let session = URLSession.shared

2. Default Configuration

This is the most commonly used session type. It uses a persistent disk-based cache, stores
credentials in the user’s keychain, follows redirects by default, and has a reasonable
default timeout value. Default session is ideal for standard HTTP requests like fetching

data from a server or submitting a form.

GrokkingSwift 69
let defaultSession = URLSession(configuration: .default)

3. Ephemeral Configuration

Similar to the default session, but it doesn’t write any data to disk. All caches, credential

stores, and so on, are kept in memory and tied to the session. Ephemeral session is
suitable for sensitive data that shouldn’t persist between sessions.

let ephemeralSession = URLSession(configuration: .ephemeral)

4. Background Configuration

This type of session allows HTTP and HTTPS uploads and downloads to be performed in

the background, even when the app is suspended, terminated, or not running. It is useful
for large file transfers, like uploading photos or downloading video files.

let backgroundSession = URLSession(configuration:


.background(withIdentifier: "sample.grokkingswift.io.backgroundSession"))

When using background configuration, there are a few things you should keep in mind:

Each configuration should have a unique identifier. This unique identifier is crucial for
correctly resuming tasks in the background.

When you implement background transfers in app extensions, remember to set the
shared container identifier on the session configuration. This is necessary because app

GrokkingSwift 70
extensions are terminated quickly, and background tasks need to continue in a shared
container.

Only upload tasks that are uploading from a file reference support background
execution. Uploading from data instances or streams won’t continue in the background.
Be cautious with the isDiscretionary property of URLSessionConfiguration. It should be

set to false for immediate uploading and downloading, as setting it to true can delay
transfers until optimal conditions are met (like device being connected to Wi-Fi and
charging)

5. Custom Configuration

You can create a custom configuration using URLSessionConfiguration and initialize a


URLSession with that configuration. This allows you to customize various settings such as

cache policy, timeout values, HTTP headers, network service type, and more. Here’s an
example:

let configuration = URLSessionConfiguration.default


configuration.timeoutIntervalForRequest = 30
configuration.httpAdditionalHeaders = ["Custom-Header": "Value"]
let session = URLSession(configuration: configuration)

29 - Discuss the different approaches to persist data in iOS


application

Purpose: Assesses candidate’s knowledge and decision-making skills regarding the

various data persistence options available in iOS, such as UserDefaults, Core Data,
file system, and Keychain, and when to use each.

GrokkingSwift 71
1. UserDefaults

UserDefaults is designed as a simple key-value store, offering lightweight mechanism for

storing small amounts of data, like user preferences, flags for first-time app usage, user’s
preferred app theme, or user’s last-selected tab.

UserDefaults is easy to use, but not suitable for large or complex data, or sensitive data
that needs encryption.

2. Property List (plist)

Plists are used for storing user configurations and small amounts of data in a structured
file format. They are more flexible than UserDefaults as the data can be edited manually.
Usually, Plists are ideal for data that needs more structure than UserDefaults can provide,

such as a list of countries or settings options.

Like UserDefaults, plists are not meant for large data sets or sensitive data. They also
require manual parsing and aren’t as efficient for read/write operations compared to

databases.

3. File System (Documents and Caches Directory)

This is a low-level way to store any kind of data, such as images, videos, documents, or

custom binary formats. File system lets you access the app’s sandbox directory, where you
can create, read, write, or delete files and folders. File system gives you full control over

the data, but requires more code and error handling, and does not provide any features
like querying, filtering, or caching.

4. JSON

GrokkingSwift 72
Codable protocol and JSON offer a sophisticated approach to persist custom data models
in iOS application. This method involves encoding your complex data structures or objects

into JSON format and subsequently writing them to the device’s disk. One of the key
strengths of using Codable with JSON is its flexibility, making it ideal for handling intricate
objects and their relationships. However, it’s important to note that this technique requires

additional coding effort, particularly for the encoding and decoding processes.

5. Keychain

Keychain is a secure storage mechanism provided by Apple for storing sensitive data, such

as passwords, tokens, or certificates. Keychain encrypts the data and protects it with the
device’s passcode or biometric authentication. Keychain also allows sharing data across
apps from the same developer or team. Keychain is ideal for storing small amounts of data

that need high security, but not for storing large or complex data, or data that needs to be
accessed frequently or quickly. In addition, the Keychain API can also be complex and

cumbersome for basic storage needs.

6. SQLite

This is a way to store and manipulate relational data using the SQLite database engine.

SQLite lets you create, read, update, and delete data using SQL statements, and supports
transactions, indexes, triggers, and views. SQLite is fast, lightweight, and reliable, but
requires more code and error handling, and does not provide any object-oriented features,

such as inheritance, polymorphism, or relationships.

Directly using SQLite involves dealing with low-level C APIs, which can be error-prone.
Instead, you would usually utilize FMDB - a popular SQLite wrapper in Objective-C that

simplifies SQLite’s interface. FMDB alleviates the complexity in dealing with low level APIs
but still requires SQL knowledge and setting up migrations for schema changes.

GrokkingSwift 73
7. Core Data

Core Data is Apple’s framework for object graph management and persistent storage. It

shines in applications with complex data models, such as an app with a sophisticated user
profile system or an e-commerce app that needs to manage a catalog of products and user

orders. It also provides object-oriented management of structured data, along with query
capabilities.

There is a steep learning curve to effectively use Core Data. It also adds complexity to the

app, making it potentially overkill for simple storage needs.

8. Realm

This is a third-party framework that provides an alternative to Core Data and SQLite.

Realm is a cross-platform solution that stores and manages data using objects, and
supports features like encryption, synchronization, notifications, and reactive
programming. Realm is easy to use, fast, and scalable, but requires adding an external

dependency to the project, and may have compatibility or migration issues with future iOS
updates or versions.

9. SwiftData

SwiftData is an innovative framework tailored for data persistence in iOS applications,


built on top of Core Data. It stands out by offering more simplified, modern, and

declarative APIs, thus enhancing the overall development experience, particularly for
developers working with SwiftUI. The framework excels in providing automatic persistence
and efficient change tracking, thereby reducing the manual overhead typically associated

with data management.

GrokkingSwift 74
However, SwiftData is relatively new in the landscape of iOS development tools (it
requires iOS 17 or higher), meaning it has a smaller community when compared to the

well-established Core Data.

Each data persistence method in iOS serves different needs and comes with its own set of

trade-offs. The choice depends on the specific requirements of your application, the nature
of the data, the complexity of the data model, and the need for security.

30 - Explain how you would model a many-to-many

relationship in Core Data.

Purpose: Assesses the candidate's knowledge of data modeling within Core Data
and their ability to handle complex relationships efficiently.

In database design, a many-to-many relationship occurs when multiple records in one


table are associated with multiple records in another table. For example, consider an

application that tracks books and authors. A single book can have multiple authors, and a
single author can write multiple books. This scenario exemplifies a many-to-many
relationship.

Unlike direct one-to-many relationships, Core Data doesn't explicitly provide a many-to-
many field type. However, you can achieve it through Core Data’s graphical data model

editor. Below is a concrete example:

First, we need to define our entities in Core Data Model Editor. Let’s say Book and

Author
For each entity, create a relationship that points to other entity. In the Book entity,
create a relationship named authors that points to the Author entity. Similarly, in the

GrokkingSwift 75
Author entity, create a relationship named books that points to the Book entity.
Core Data needs to know that these relationships are to-many. In the model editor,
select the relationship and check the To-Many Relationship box in the Data Model

Inspector.
Core Data requires that relationships have an inverse to maintain referential integrity.

For the authors relationship in the Book entity, set its inverse to the books relationship
in the Author entity, and vice versa.
Core Data handles many-to-many relationships by automatically managing an implicit

intermediate join table behind the scenes. This mechanism allows developers to work
directly with the high-level objects without concerning themselves with the join logic.
With the relationships configured, you can now add or remove objects from these

relationships using Core Data’s generated accessor methods or by manipulating the


sets directly.

31 - Describe Managed Object Context in Core Data

Purpose: Tests candidate’s understanding of Core Data, specifically how the

Managed Object Context acts as an in-memory workspace for managing model


objects and their life cycle within a Core Data stack.

Managed Object Context (MOC) is essentially the heart of Core Data. Think of a MOC as a
workspace or a buffer zone where you interact with and manipulate managed objects
(your data entities). It’s like a scratch pad containing a collection of objects that you’ve

fetched or created and are working with. MOC manages the life cycle of these objects,
including fetching data from the store, inserting new objects, deleting objects, and saving
changes back to the store.

GrokkingSwift 76
Imagine an app that manages a list of tasks:

When you fetch tasks from Core Data, they are retrieved into a MOC. You can then
modify these tasks (like updating the status or details) directly within the context

let context = persistentContainer.viewContext


let fetchRequest = NSFetchRequest<Task>(entityName: "Task")
let tasks = try context.fetch(fetchRequest)

When you create a new task, you insert it into the context. Until you save the context,

this task is only in the context, not in the persistent store.

let newTask = Task(context: context)


newTask.title = "New Task"

When you’re ready to persist your changes, you save the context. This writes the
changes to the persistent store.

if context.hasChanges {
try context.save()
}

If you decide to discard any changes made in the context before saving, you can simply

not save the context or explicitly undo the changes.

Core Data typically uses one or more MOCs, which can have different types

(NSMainQueueConcurrencyType for main-thread operations and


NSPrivateQueueConcurrencyType for background operations). You can utilize different

GrokkingSwift 77
contexts for UI updates and background processing. However, keeping them in sync can be
challenging.

32 - How do you ensure thread safety when accessing or


modifying data in a concurrent programming environment

in iOS?

Purpose: Tests candidate’s understanding of concurrency and multithreading in iOS,


including the use of GCD and OperationQueues, and strategies to prevent data
races and deadlocks.

Ensuring thread safety in a concurrent programming environment in iOS is crucial to


prevent data races, deadlocks, and other concurrency issues that can lead to unpredictable

behavior, crashes, or corrupted data. Swift and iOS provide several mechanisms to manage
access to shared resources and ensure thread safety effectively.

1. Serial Dispatch Queues

Using serial dispatch queues (often through GCD) is a fundamental way to ensure that a
block of code is executed at a time, thus preventing simultaneous access to shared
resources. You can create a private serial queue and dispatch all access to a shared

resource to this queue.

let serialQueue = DispatchQueue(label: "io.grokkingswift.serialQueue")

func updateSharedResource() {
serialQueue.async {
// Code to update shared resource safely

GrokkingSwift 78
}
}

2. Concurrent Dispatch Queues with Barriers

For read-write operations where reads are more frequent than writes, you can use a
concurrent queue with barriers. A barrier allows read operations to execute concurrently

but ensures that the write operation is performed alone, without any other read or write
operations executing concurrently.

let concurrentQueue = DispatchQueue(


label: "io.grokkingswift.concurrentQueue",
attributes: .concurrent
)

func readSharedResource() {
concurrentQueue.async {
// Code to read shared resource
}
}

func writeSharedResource() {
concurrentQueue.async(flags: .barrier) {
// Code to write shared resource safely
}
}

3. Synchronization Primitives

iOS and Swift provide synchronization primitives such as NSLock, NSRecursiveLock,


DispatchSemaphore , and others for coordinating the execution of code across different

GrokkingSwift 79
threads. These can be used to lock access to a shared resource, ensuring only one thread
can access the resource at any given time.

let lock = NSLock()

func updateSharedResource() {
lock.lock()
// Code to update shared resource safely
lock.unlock()
}

4. Operation Queues

OperationQueue is a high-level abstraction over GCD that can be used to manage


concurrency. You can set the maxConcurrentOperationCount to 1 to make an

OperationQueue serial, or use dependencies between operations to control the execution


order.

These are some of the ways to ensure thread safety in iOS. However, they may have some

drawbacks or limitations, such as performance overhead, complexity, or deadlock. It’s a


good practice to minimize the amount of shared mutable state and use immutable objects
where possible.

33 - Explain the concept of a barrier task in GCD.

Purpose: Gauge the candidate’s understanding of concurrency in iOS development


as well as their ability to manage and synchronize access to shared resources, and

their familiarity with GCD as a tool for creating concurrent applications.

GrokkingSwift 80
The concept of Barrier Task

A barrier task is a powerful feature of GCD that allows you to create a synchronization
point within a concurrent dispatch queue. It ensures that it is the only task executing on
the queue at that moment. When a barrier task is executing, it blocks the execution of any

other tasks on the same queue until it completes. Barrier tasks are typically used to safely
read and write to shared resources or data structures without causing data races or
inconsistencies.

How Barrier Tasks work

Barrier tasks only make sense in concurrent queues. They don't have any effect in serial
queues since tasks in a serial queue are already executed one at a time.

When a barrier task is added to a concurrent queue, it waits for previously submitted
tasks to complete and then executes. During its execution, no other tasks in the queue
are started, effectively creating a synchronization point.

Once the barrier task completes, the queue returns to its usual concurrent behavior,
executing multiple tasks simultaneously.

Here's how you use a barrier task in Swift:

let concurrentQueue = DispatchQueue(


label: "io.grokkingswift.concurrentQueue",
attributes: .concurrent
)

concurrentQueue.async {
// Task 1
}

concurrentQueue.async {

GrokkingSwift 81
// Task 2
}

concurrentQueue.async(flags: .barrier) {
// Barrier Task - This task will be executed alone
}

concurrentQueue.async {
// Task 3
}

In this example, Task 1 and Task 2 can execute concurrently. However, when the barrier
task starts, Task 3 will not begin until the barrier task completes.

A barrier task can be useful when you want to perform some tasks concurrently, but also
need to perform some tasks serially or atomically. For example, you can use a barrier task

to implement a “multi-reader, single-writer” pattern, where you allow multiple tasks to


read from a shared resource, but only one task to write to it at a time. You can also use a
barrier task to update the state of the queue, such as adding or removing tasks, or

changing the quality of service.

class Cache {
private var data = [String: Any]()
private let queue = DispatchQueue(label:
"io.grokkingswift.cacheQueue", attributes: .concurrent)

func setValue(_ value: Any, forKey key: String) {


queue.async(flags: .barrier) {
self.data[key] = value
}
}

func value(forKey key: String) -> Any? {


queue.sync {

GrokkingSwift 82
return data[key]
}
}
}

In this Cache class, the setValue(_:forKey:) method uses a barrier to ensure that the write
operation doesn't conflict with other read (value(forKey:)) or write operations.

34 - How would you perform multiple network requests in

parallel and then execute a completion handler once all


have finished?

Purpose: Evaluates the candidate's understanding of concurrency in iOS, their


ability to manage asynchronous tasks efficiently, and their skill in synchronizing
concurrent operations.

Performing multiple network requests in parallel and coordinating their completion is a


common requirement in iOS development. Both Grand Central Dispatch (GCD) and

OperationQueue offer robust solutions for handling such concurrency needs.

1. GCD with DispatchGroup

GCD is a low-level C-based API that offers a straightforward way to execute tasks
concurrently. To accomplish executing multiple network requests in parallel and then

calling a completion handler, you can use GCD's DispatchGroup. Here are the key steps:

You first need to create a DispatchGroup. This group tracks the completion of the

network requests.

GrokkingSwift 83
Before starting each network request, you call group.enter() to inform the

DispatchGroup that a task has started.


Once a network request completes, you call group.leave() to indicate that this
particular task is finished.

To notify upon group completion, you use group.notify(queue:execute:) to set up a


closure that executes once all tasks in the group have completed.

Let’s put everything together in an example below:

let dispatchGroup = DispatchGroup()


let queue = DispatchQueue.global(qos: .background)

let urls = [URL(string: "https://grokkingswift.io/api/endpoint1")!,


URL(string: "https://grokkingswift.io/api/endpoint2")!,
URL(string: "https://grokkingswift.io/api/endpoint3")!]

for url in urls {


dispatchGroup.enter()
queue.async {
URLSession.shared.dataTask(with: url) { data, response, error in
defer { dispatchGroup.leave() }
// Process the response/data/error here
}.resume()
}
}

dispatchGroup.notify(queue: .main) {
// All requests have completed; execute completion handler
print("All network requests have finished.")
}

GrokkingSwift 84
2. OperationQueues with Dependencies

OperationQueue is a high-level API that manages a queue of Operation objects. It


provides more control over task execution, such as setting dependencies between

operations and managing operation priorities. Here are the key steps:

First, you need to create an Operation for each request. You can either subclass
Operation or use BlockOperation to encapsulate the request.

The next step is to add operations to an OperationQueue. This automatically starts


their execution in parallel, depending on the system’s current load and the queue’s

maxConcurrentOperationCount.
Finally, create another Operation that acts as a completion handler, then set it to
depend on all the network request operations, ensuring it executes last.

let operationQueue = OperationQueue()


let completionOperation = BlockOperation {
print("All network requests have finished.")
}

let urls = [URL(string: "https://grokkingswift.io/api/endpoint1")!,


URL(string: "https://grokkingswift.io/api/endpoint2")!]

for url in urls {


let requestOperation = BlockOperation {
URLSession.shared.dataTask(with: url) { data, response, error in
// Process the response/data/error here
}.resume()
}
completionOperation.addDependency(requestOperation)
operationQueue.addOperation(requestOperation)
}
operationQueue.addOperation(completionOperation)

GrokkingSwift 85
3. Using Async/Await with Task Groups

Swift's structured concurrency model allows you to group multiple asynchronous

operations using a TaskGroup. Here’s how you can perform multiple network requests in
parallel and wait for all to complete:

func fetchAllData(urls: [URL]) async {


do {
try await withThrowingTaskGroup(of: (Data?, URLResponse?,
Error?).self, body: { group in
// Add each network request to the task group
for url in urls {
group.addTask {
return try await URLSession.shared.data(from: url)
}
}

// Process results from each request as they complete


for try await (data, response, error) in group {
// Process each response
}

// All tasks in the group have completed at this point


})

// Completion handler logic after all requests have finished


print("All network requests are finished.")
} catch {
// Handle any errors that occurred during any of the network
requests
print("An error occurred: \(error)")
}
}

GrokkingSwift 86
Important Considerations

To ensure thread safety, you must update any UI elements from the main queue after
asynchronous work completes.

To enhance user experience, you should implement robust error handling within your
network tasks and propagate errors up for consolidated handling.
Last but not least, monitor the number of simultaneous requests to avoid

overwhelming the network or the server.

35 - How do you use DispatchQueue in Swift for executing


tasks concurrently?

Purpose: This question probes the candidate's understanding of Grand Central


Dispatch (GCD) for managing concurrent and serial task execution, emphasizing
when and why to use each approach for optimizing app performance and

responsiveness.

DispatchQueue in Swift, part of the Grand Central Dispatch (GCD) framework, is a powerful

tool for managing the execution of tasks, either serially or concurrently, on different
threads. Using DispatchQueue, you can improve your app's responsiveness by offloading
intensive tasks from the main thread, and executing multiple tasks in parallel.

There are primarily two types of queue:

Serial Queue: Executes one task at a time in the order they are added to the queue.
Each task starts only after the previous one finishes.
Concurrent Queue: Allows multiple tasks to run at the same time. The tasks start in the

order they are added, but they can finish in any order.

GrokkingSwift 87
Every app also has a main queue, which is a special serial queue that runs on the
main thread of your application. It's primarily used for UI updates and handling
user interactions.

Besides these, GCD also provides a few predefined global concurrent queues. These
are accessible to all parts of your app and come with different Quality of Service

(QoS) levels like .background, .userInitiated, .userInteractive, and .utility.

To use DispatchQueue for executing tasks concurrently, you would typically use one of the

global concurrent queues provided by the system or create your own concurrent queue.

Using a Global Concurrent Queue:

DispatchQueue.global(qos: .background).async {
// Perform your background code here
}

Creating and Using a Custom Concurrent Queue:

let concurrentQueue = DispatchQueue(


label: "io.grokkingswift.concurrentQueue",
attributes: .concurrent
)

concurrentQueue.async {
// Task 1
}

concurrentQueue.async {
// Task 2
}

GrokkingSwift 88
Using a concurrent queue would be more beneficial than a serial queue when you need to

perform multiple heavy tasks that do not depend on each other’s results. For instance,
suppose you are developing an app that needs to process a set of images, such as applying
filters or resizing them. Processing each image can be time-consuming, and doing this

work serially on a single thread would lead to a poor user experience, as the UI would
become unresponsive. Instead, you would want to use a concurrent queue.

let imageProcessingQueue = DispatchQueue(


label: "io.grokkingswift.imageProcessing",
attributes: .concurrent
)
let images = [
UIImage(named: "Image1"),
UIImage(named: "Image2"),
UIImage(named: "Image3")
]

for image in images {


imageProcessingQueue.async {
let processedImage = applyFilter(to: image)
DispatchQueue.main.async {
// Update the UI with the processed image
}
}
}

In this example, applyFilter(to:) represents a hypothetical function that processes an

image. Each image is processed in parallel on the imageProcessingQueue, which can


significantly reduce the total processing time compared to processing them serially. Once
an image is processed, the code to update the UI with the processed image is dispatched

back to the main queue. The main thread remains responsive in the entire process.

GrokkingSwift 89
36 - Why storing large amounts of data in UserDefaults can

lead to performance issues?

Purpose: Evaluates candidate’s understanding of the UserDefaults system and its

appropriate use cases, particularly how misusing it for large data storage can
impact app performance and startup time.

Storing large amounts of data in UserDefaults can lead to performance issues in iOS
applications for several reasons:

UserDefaults is designed as a lightweight solution for storing user preferences or small

pieces of data. It operates on a property list (plist) file, which is essentially a file-based
storage for key-value pairs.

When an app accesses UserDefaults, it loads the entire plist file into memory. If this file
is large, it can consume significant memory resources, leading to increased memory
usage of your app.

Each time a value is set in UserDefaults, the entire plist file is written back to disk.
Frequent changes to UserDefaults mean frequent writes of the entire plist file, even for
minor updates. These can be resource-intensive operations, particularly for large data

sizes, leading to poor write performance.


UserDefaults lacks advanced data management features like indexing, querying, or
efficient data retrieval mechanisms. As a result, searching or modifying large amounts

of data can be highly inefficient.

As can be seen from the reasons above, UserDefaults is not optimized for handling large

datasets. The performance issues mainly stem from its entire file loading mechanism,
inefficient write operations, and the lack of sophisticated data management capabilities.

GrokkingSwift 90
For applications dealing with substantial amounts of data, it's advisable to use more

robust data storage solutions.

37 - Describe how you would optimize the performance of a

slow-scrolling UITableView or UICollectionView in an iOS


app.

Purpose: Tests the candidate's ability to diagnose and solve common performance

issues in iOS apps, emphasizing understanding of cell reuse, layout optimization,


and asynchronous data loading.

Optimizing the performance of a slow-scrolling UITableView or UICollectionView is crucial


for maintaining a smooth user experience in an iOS app. Performance issues in these
views are often due to inefficient data handling, excessive layout calculations, or heavy

drawing operations. There are several strategies that you can apply to improve the
performance:

1. Efficient Data Loading

Load data as needed rather than all at once. For images or data fetched from the
network, consider loading them asynchronously and caching the results for quicker
access.

Utilize the prefetching APIs (UITableViewDataSourcePrefetching,


UICollectionViewDataSourcePrefetching) to load data before it's needed, reducing
waiting times.

GrokkingSwift 91
2. Reuse Cells

Ensure that cells are reused efficiently by dequeuing them properly using
dequeueReusableCell(withIdentifier:) for UITableView or

dequeueReusableCell(withReuseIdentifier:for:) for UICollectionView. Reusing cells


minimizes allocation and layout costs for new cells.

3. Optimize Cell Layout and Content

Reduce the number of views and layers in each cell. Complex hierarchies increase the
time it takes to render the cell.
While Auto Layout is powerful, it can be slow for complex cell layouts. Consider using

manual layout calculations in layoutSubviews for performance-critical views.


For custom cell views, ensure drawing code is optimized and avoid transparency and
blending where possible. Use shouldRasterize in layers judiciously to cache complex

cell content.

4. Image Handling

Scale down large images to the size they will be displayed at before showing them in

cells. Doing this asynchronously prevents blocking the main thread.


Use appropriate image formats and compression to reduce the memory footprint of
images used within cells.

5. Efficient Scrolling

Whenever possible, specify a fixed row height for UITableView cells by setting the
rowHeight property. This avoids the need to calculate the height of each cell

dynamically.

GrokkingSwift 92
Minimize offscreen rendering by simplifying cell designs and using opaque views.

6. Background Processing

Ensure that any heavy lifting, such as data processing or image loading, is done in the

background. Use Grand Central Dispatch (GCD) or OperationQueue to manage these


tasks.

7. Profiling and Debugging

Use Xcode's Instruments tool, particularly the Time Profiler and Core Animation tools,
to identify bottlenecks in rendering, layout, and CPU usage.
Enable debugging options in the Simulator and on the device to visually inspect issues

related to layout and rendering, such as slow animations and color blending.

By systematically addressing each potential area of inefficiency, you can significantly

improve the scrolling performance of UITableView and UICollectionView in your iOS app,
ensuring a smooth and responsive user experience.

38 - How would you optimize app launch time?

Purpose: Assesses the candidate’s approach to optimizing critical app metrics, such
as startup time, including understanding of profiling tools and optimization
techniques.

The app launch process begins when the user taps the app icon and ends when the app’s
initial UI appears on the screen. Launching an app involves two main scenarios: cold

launch and warm launch. Each requires different optimization strategies.

GrokkingSwift 93
Cold Launch

Cold launch occurs when the app starts from scratch. It happens in several cases:

The app is launch for the first time after being installed.
The app is relaunched after being completely terminated or after a device reboot.
The app has been purged from memory due to system memory pressure.

This process typically takes longer than a warm launch as it involves full initialization:
loading the app into memory, initializing the runtime, linking libraries, setting up the main

run loop, and creating the app’s initial state.

Optimization Strategies For Cold Launch:

Reduce the number of dynamic libraries and frameworks, and consider using static
libraries where appropriate.
Keep code in the app delegate lightweight and offload heavy tasks to be executed after

the launch.
Delay the loading of non-essential resources until after the app has launched. Non-
critical operations such as checking for updates, sending analytics could also be

delayed after launch.


Simplify the initial view controller and use a light weight launch screen. If you are

using complex storyboards, instead of putting everything into a single massive


storyboard, break down them into smaller, more manageable ones. Consider using
programatic UI where it makes sense.

Perform heavy computations in the background and minimize the workload on the
main thread. If you’re initializing various services (like location, networking, etc.),
consider doing this after the initial view is displayed.

GrokkingSwift 94
Some third-party libraries can significantly impact launch time. Audit and evaluate the
impact of each third-party library. Remove or replace those that are inefficient. The
fewer dependencies your app has, the faster it will take to load.

You can run your app with the environment variable DYLD_PRINT_STATISTICS = 1 to
measure time spent on dynamic linking and loading libraries.

Regularly use Instruments to identify and optimize slow areas during the cold launch.
Regularly check and remove unused resources such as images, libraries, or code. The
goal is to keep the app size as small as possible.

Warm Launch

A warm launch occurs when the app resumes to the foreground from a suspended (still in
memory but not executing any code) state. This process is faster than a cold launch as it

doesn’t require full initialization, many resources are already loaded in memory.

Optimization Strategies For Warm Launch:

Utilize State Preservation and Restoration to efficiently save the state of the user
interface when the app goes into the background and restore it upon return.
Manage memory usage wisely to reduce the chances of the app being terminated while

in the background. For instance, if your app has cached data or large in-memory
structures, consider releasing some of these resources.
If your app is performing background tasks (like fetching data, running location

updates), ensure they are managed properly to not drain battery life or system
resources.

Avoid long block operations or UI updates when the app enters the foreground. The
goal is to resume quickly and seamlessly.

GrokkingSwift 95
While cold launches involve the full initialization process of the app, warm launches deal

with bringing an already-initialized app back to an active state. Optimizing cold launches
often revolves around reducing initial load times, while optimizing warm launches focuses
on efficient state management and memory usage. By profiling and optimizing for both

cold and warm launches, you can significantly improve the overall user experience of your
app.

39 - How do you optimize Core Data performance for a large

dataset?

Purpose: Tests the candidate's understanding of Core Data and their ability to
optimize data fetching and storage to improve app performance and user

experience.

Before getting into the strategies for optimizing Core Data when dealing with substantial

amounts of data, let’s understand some typical bottlenecks.

You are pulling more data from the database than you currently need.

Each time you access a property of an unfetched Core Data object (a fault), it triggers a
trip to the database.
Fetch requests that are non-specific or lack smart predicates can slow things down.

Core Data interactions should ideally occur on a background thread to avoid blocking
the main (UI) thread.

Having identified these potential trouble spots, let's explore solutions to make Core Data
more efficient with large datasets.

GrokkingSwift 96
1. Efficient Fetching

A fundamental step in optimizing Core Data is refining how data is fetched. Efficient
fetching reduces memory usage and processing time, significantly impacting overall

performance.

You can start with specifying fetch predicates and sort descriptor to narrow down the
fetched data to exactly what’s needed. This precision avoids the overhead of

processing excessive data.


Instead of fetching all data at once, use fetchBatchSize on your fetch requests to

control the number of objects loaded into memory, preventing memory spikes.
Use fetchProperties to restrict fetching objects to only the properties you’ll actually
display, making them lighter.

You can employ relationshipKeyPathsForPrefetching to tell Core Data in advance about


the relationships between objects you’ll likely need, preventing excessive individual
fetches later on.

2. Optimizing Managed Object Contexts

After addressing fetching strategies, the next critical area involves the management of
contexts. They key approach is to utilize background contexts for operations that are

resource-intensive, ensuring that the UI remains responsive by offloading work to


background threads. When changes occur on background contexts, be careful merging
back to the main context for display.

3. Efficient Data Models

The structure of your data model can also influence Core Data performance. An optimized
data model facilitates faster data access and manipulation.

GrokkingSwift 97
For frequently used, read-heavy data, maintain a lightweight cache (dictionary of
custom solution) to reduce database hits.
For model updates, lightweight migrations offer a performance-friendly way to evolve

your data schema over time.


If some value can be calculated from existing data, store it as a calculated attribute in
Core Data to avoid complex on-the-fly computations.

4. Indexing and Caching

With a well-structured data model in place, further performance gains can be achieved
through indexing and caching. While choosing and indexing attributes that are frequently

queried or sorted can improve the speed of fetch requests at the cost of increased storage,
caching frequently accessed data in memory can significantly reduce the need for
repeated fetches.

5. Leveraging Performance Tuning Tools

Optimizing Core Data is an iterative process that benefits greatly from continuous
monitoring and analysis. Performance tuning tools like Instruments are essential for

identifying bottlenecks and areas for improvement. Furthermore, you can use Core Data’s
built-in debugging options to analyze and optimize SQL queries generated by Core Data.

40 - Explain the Singleton pattern and its implications in

iOS app development

Purpose: Evaluates the candidate’s knowledge of design patterns, specifically the

Singleton, including when its use is appropriate and potential drawbacks.

GrokkingSwift 98
The core idea of a Singleton is simple: you want to ensure only one instance of a
particular class exists throughout your entire application. It becomes a central access

point for whatever data or functionality it provides.

In Swift, a Singleton can be implemented using a static property and a private initializer to

prevent other parts of the code from creating a new instance of the class.

class MySingleton {
static let shared = MySingleton()
private init() {}

// Additional functionality here


}

Singletons are prevalent in iOS development. They're used to provide a controlled access

point to shared resources, configurations, or operations that require a single point of


coordination across an application. Since the Singleton instance is typically kept alive for
the duration of the application’s lifecycle, it’s useful for managing long-lived objects that

need to persist. Common examples include:

UIApplication.shared manages the app's main event loop and other top-level app

behaviors.
UserDefaults.standard provides a simple interface for persisting user preferences.
NotificationCenter.default: Manages the delivery of notifications within a program.

FileManager.default: Provides common file system operations throughout your app.

Beside the advantages, Singleton’s use comes with some concerns:

Singletons introduce a global state into an application, which can make the app's
behavior less predictable and harder to debug.

GrokkingSwift 99
Singletons can make unit testing more challenging, as the state of the singleton can
persist across tests, leading to dependencies between tests and making tests less
reliable.

Components that rely on singletons become tightly coupled to them, making the code
less modular and harder to refactor or replace those components.
In multi-threaded apps, more effort is needed to ensure that singletons are thread-

safe. The typical approach involves using dispatch queues or locks to synchronize
access to the singleton’s resources.

In practice, singletons should be used sparingly and only when there is a clear justification
for their use. In most case, developers should consider using dependency injection to pass
singleton instances to components during testing. This allows you to replace real

singletons with mock objects, improving test isolation and reliability.

41 - Explain the delegation pattern and its use cases in iOS

development. Discuss the alternative approaches and their


benefits.

Purpose: Assesses the candidate's grasp of one of the foundational design patterns

in iOS development, their ability to implement it, and knowledge of alternative


patterns like notifications or closures for callback mechanisms.

The delegation pattern is a fundamental design pattern used extensively in iOS


development. It allows one object to delegate responsibility for certain tasks to another
object, without needing to know the specifics of how those tasks are carried out. This

GrokkingSwift 100
pattern facilitates communication between objects in a way that reduces coupling and

enhances flexibility and reusability of code.

The delegation pattern involves defining a protocol that specifies the methods a delegate

may or must implement. An object needing delegation (the delegator) holds a reference to
another object (the delegate) that conforms to the protocol. When a specific event occurs,
the delegator informs the delegate by calling the appropriate method defined in the

protocol.

Some common use cases of the delegation pattern in iOS development are:

UITableView and UICollectionView: These classes use delegates to provide data for
their cells, handle user interactions, and customize their appearance
UITextField and UITextView: These classes use delegates to validate, format, and

manipulate the text input by the user.


CLLocationManager: This class uses a delegate to report location and heading updates
to the app.

Benefits of the Delegation Pattern

Decoupling: Delegation helps in decoupling the delegate from the delegator, as the
delegator doesn't need to know the concrete class of the delegate, just the protocol it

conforms to.
Single Responsibility Principle: It allows for the separation of concerns, adhering to the

single responsibility principle by enabling an object to delegate responsibility to


another object.
Flexibility and Reusability: The pattern provides flexibility in the implementation of

the delegated responsibilities and promotes reusability of both the delegator and
delegate objects.

GrokkingSwift 101
Alternative: Closure-based Callbacks

An alternative to the delegation pattern is using closure-based callbacks. This approach


involves defining a closure property within a class and allowing the class's consumer to
provide a block of code that should be executed at a specific point in the class's operation.

Some benefits of the closure-based callbacks over the delegation pattern are:

Simplicity: Closures can simplify the code needed to communicate between objects,
especially when the interaction is straightforward or involves only a single callback.
Inline Context: Closures allow for the handling code to be written inline, which can

make it easier to understand the flow of information and logic without needing to
navigate to separate delegate method implementations.
Flexibility: They provide a flexible way to pass around chunks of executable code,

which can be particularly useful for asynchronous tasks, such as network requests.

Alternative: Observer Pattern

An another alternative approach to the delegation pattern is the observer pattern, which is

implemented by the NSNotificationCenter class in iOS. The observer pattern allows one-
to-many communication between objects, where an object (the subject) broadcasts a
notification to multiple objects (the observers) who have registered for that notification.

The observers can then respond to the notification accordingly.

Some benefits of the observer pattern over the delegation pattern are:

Decoupling: The subject does not need to know anything about the observers, and the
observers do not need to conform to a specific protocol. This reduces the coupling and

dependency between objects, and makes the code more reusable and flexible.

GrokkingSwift 102
Scalability: The observer pattern can handle multiple observers for the same

notification, whereas the delegation pattern is limited to one delegate for each
delegator. This makes the observer pattern more suitable for scenarios where multiple

objects need to be notified of the same event.

42 - Describe the MVVM architecture. How does it compare


to the traditional MVC pattern used in iOS apps?

Purpose: This question evaluates the candidate's architectural knowledge,

including the ability to implement MVVM in Swift, the benefits of using it over
MVC, and understanding of data binding mechanisms.

The Model-View-ViewModel (MVVM) architecture is a design pattern that separates the


presentation layer from the business logic and data layer. It consists of three components:

Model: Represents the data and business logic of the application. It is similar to the
model in MVC and is responsible for fetching, storing, and manipulating data, often
from a database or an API.

View: Represents the UI elements and layout of the application. The View is
responsible for displaying visual elements and animations to the user. In MVVM, the
View binds to observable variables in the ViewModel and reacts to changes, updating

the UI accordingly.
ViewModel: Acts as the link between the Model and the View. It receives input from
interactions on the View and processes these inputs via the Model, returning outputs

(such as formatted data) that can be displayed by the View.

GrokkingSwift 103
Model-View-Controller (MVC) is another architectural pattern commonly used in iOS
development. MVC divides an application into three interconnected parts to separate

internal representations of information from the ways that information is presented to and
accepted from the user.

Model: Similar to MVVM, it represents the application's data and business logic.
View: Corresponds to the UI components, but in MVC, the View is more passive, waiting
to be updated rather than actively observing changes.

Controller: Acts as an intermediary between the Model and the View, processing all the
business logic related to the view and updating the View with new data.

GrokkingSwift 104
The Key Differences between MVVM and MVC

MVVM provides a better separation of concerns by removing most of the business logic

from the View to the ViewModel. This reduces the complexity of the View and makes
the ViewModel easier to test. In contrast, controllers in MVC often become bloated with
both business logic and view logic, making them harder to maintain and test.

MVVM relies heavily on data binding, allowing for automatic updates between the
View and ViewModel. Changes in the underlying data model can automatically reflect
in the View without additional code. MVC lacks this built-in mechanism, often requiring

manual setup to synchronize the View with the Model.


MVVM can lead to easier maintenance as the project grows. However, because MVVM
requires more boilerplate code to implement the architecture, it may introduce more

complexity in smaller projects where the straightforward approach of MVC could


suffice. In addition, the view model needs to communicate with the view using

bindings, observers, or callbacks. This increases the risk of memory leaks or retain
cycles.

GrokkingSwift 105
When to Use MVVM over MVC

Complex User Interfaces: MVVM can manage complex UIs better due to its data binding
and separation of view logic into the ViewModel.

Dynamic Content: Applications with dynamic content that changes frequently based on
user interactions or live data updates can benefit from MVVM's data binding and

automatic UI updates.
Testability Requirements: Projects where unit testing and test coverage are critical
might favor MVVM for its improved testability of application logic without relying on

UI components.

While MVC has been the traditional choice for many iOS applications, the introduction of
MVVM offers developers a robust alternative, especially for applications requiring

sophisticated user interfaces and interactions. Choosing between MVC and MVVM depends
on the specific needs of the project, the complexity of the UI, and the team's familiarity

with the patterns.

43 - What is the difference between throws and rethrows?

Purpose: Evaluates candidate’s understanding of error handling in Swift,


particularly distinguishing between functions that can throw errors themselves and

those that propagate errors thrown by their closure parameters

In Swift, throws is used with functions or methods to indicate that they can propagate

errors that occur within the function. Functions with throws can contain multiple points
where different errors can be thrown, providing flexibility in handling various error
conditions. The caller of the functions or methods marked with throws must handle these

errors using do-catch blocks or propagate them further.

GrokkingSwift 106
Example of a throwable function:

enum FileError: Error {


case invalidFilePath, fileNotFound, unreadable
}

func readFile(at path: String) throws -> String {


if path.isEmpty {
throw FileError.invalidFilePath
}

// Throw `fileNotFound` or `unreadable` if the file cannot be located


or opened.

// Assume file is read successfully


return "File contents"
}

do {
let fileContents = try readFile(at: "/path/to/file")
print(fileContents)
} catch {
print("Error reading file: \(error)")
}

The rethrows keyword is used when a function takes one or more throwing functions as

parameters. A function marked with rethrows only throws an error if one of its function
parameters throws an error. It does not generate errors on its own.

func processFiles(filePaths: [String], fileProcessor: (String) throws ->


String) rethrows {
for filePath in filePaths {
let processedFile = try fileProcessor(filePath)
print(processedFile)
}

GrokkingSwift 107
}

do {
try processFiles(filePaths: ["/path/to/file1", "/path/to/file2"],
fileProcessor: readFile)
} catch {
print("An error occurred: \(error)")
}

In this example, processFiles(filePaths:fileProcessor:) is a function that takes a throwing

function as a parameter (fileProcessor). It only throws an error if fileProcessor (in this case,
readFile) throws an error. If fileProcessor does not throw, then processFiles does not throw

either.

Overall, when you see throws in a function declaration, you know that the function itself
may throw an error. When you see rethrows, you know that the function may throw an

error inly if one of its throwing function parameters throws an error.

44 - Describe your approach to writing unit tests for a

UIViewController.

Purpose: Checks the candidate’s experience with Test-Driven Development (TDD) in


iOS, specifically their strategies for testing UI components and mocking
dependencies.

In a typical iOS app, the view controllers are the components that frequently react to
network responses, user input, timers, etc. Thus, your unit tests should emphasize on the

controller’s behavior in response to various inputs and its interaction with external

GrokkingSwift 108
dependencies. The goal is to verify that the controller behaves as expected under different
scenarios.

Here are some aspects that you can consider to test in a UIViewController:

User Interaction and State Transformations: Test how the controller responds to user

actions, such as tapping a button or entering text in a text field. This involves
simulating the actions and verifying the expected outcomes. Does the view controller
correctly change its internal state and potentially modify its views upon particular

events or inputs (e.g., does data loading set a correct loading state?)
Lifecycle Events: Ensure that the controller correctly handles lifecycle events. For

example, it might fetch data on viewDidLoad or start/stop observing notifications on


viewWillAppear and viewWillDisappear.
Data Binding and Presentation Logic: Verify that the UI correctly reflects the underlying

data model, particularly after updates. This includes testing dynamic UI changes, such
as table view updates in response to data changes.
Navigation: Test the controller's navigation logic, ensuring that it properly triggers

segues or programmatically navigates to other controllers in response to user actions.


Error Handling: Confirm that the controller properly handles errors, such as displaying
error messages or retry options to the user.

The external dependencies, such as network services or data managers, need to be


mocked to isolate the controller from external systems during testing. The strategy is:

Define protocols for your external dependencies and make your controller depend on
these protocols rather than concrete implementations. Then, create mock

implementations of these protocols for use in your tests.


Inject mock dependencies into the controller when testing. This can be done via the
controller’s initializer, properties, or through method parameters, depending on your

GrokkingSwift 109
design.
Use stubs to provide predefined responses from dependencies and verify that the
controller interacts with the dependencies as expected.

By following this approach, you can systematically test the various aspects of a
UIViewController, ensuring that it behaves as expected, handles user interactions correctly,
and integrates properly with the rest of the application.

45 - How do you write unit tests for network calls in an iOS


app?

Purpose: Checks the candidate's proficiency in writing testable code and their

understanding of testing strategies, essential for ensuring app reliability and


maintainability.

The core idea of unit testing is to isolate and test the smallest possible units of code (such
as a single function or method). This keeps tests focused, fast, and helps pinpoint the
exact location of issues.

In iOS development, network calls involve your app communicating with remote servers
for data exchange. Your unit test cases should focus on how your app’s code processes this

data, not on the actual network operations. The strategy is to decouple your network
request logic from the rest of your app using abstractions, then use mocks or stubs to
simulate network responses. Mock responses allow us to fully control the data flow during

tests, enabling simulation of various network conditions. Let's walk through how you
might approach this.

GrokkingSwift 110
First, abstract your network request logic behind a protocol (e.g.,
NetworkServiceProtocol). This allows you to use different implementations of the

protocol for production and testing.


Next, create a mock version of NetworkServiceProtocol that you can control in your

tests (e.g., MockNetworkService). This mock service will not make actual network
requests but will return predefined data or errors. In your unit tests, configure
URLSession to use this custom protocol by utilizing Dependency Injection.

The final step is to create sample data files (usually JSON) mirroring expected server
responses. Load these files within your mock implementation to feed back to your app
logic.

Here is the outline of the testing approach:

protocol NetworkServiceProtocol {
func fetchUserDetails(
userId: Int,
completion: @escaping (Result<User, Error>) -> Void
)
}

// Test implementation
class MockNetworkService: NetworkServiceProtocol {
// Load predefined data, simulate errors, etc.
}

class ViewModel {
private let networkService: NetworkServiceProtocol

// Inject the mock service via initializer


init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}

func fetchUserDetails(userId: Int) { ... }

GrokkingSwift 111
}

// Test Case
class NetworkTests: XCTestCase {
func testFetchUserDetailsSuccess() {
let mockService = MockNetworkService()
let viewModel = ViewModel(networkService: mockService)

viewModel.fetchUserDetails(userId: 123) { result in


// Assertions to verify correct handling of results
}
}
}

What about Error Handling?

It’s a good practice that your sample data should trigger different error types (e.g.,
HTTP status codes in the 4xx or 5xx range, malformed JSON, empty responses). This

specifically targets error handling paths in your code.


Assertions in your tests must not only check if an error occurred, but also that your app
reacts appropriately. Does it display a meaningful message? Attempt a retry? Fall back

to cached data?
Your networking layer itself should return meaningful errors that distinguish issues like
timeouts, no internet connectivity, and various server-side problems. Your unit tests

should then verify correct propagation of these errors to the relevant part of your app.

The Limitation

While unit tests give us precise control and a focus on our code logic, they can't

perfectly replicate real-world network behavior. Factors like network

GrokkingSwift 112
latency, intermittent failures, and unexpected server response changes aren't accounted

for in their purest form.


Complex mock setups can increase the maintenance burden of tests. Finding a balance
between sufficient mocking detail and keeping tests easily understandable is

important.

Best Practices

When designing your classes and services, keep dependency injection in mind. It makes

mocking and testing much easier.


Your mock objects should clearly define the expected inputs and outputs for each test
case to ensure tests are easy to read and understand.

Mocks should be targeted to test specific scenarios rather than trying to simulate every
possible network nuance.
While mocking is powerful, overuse can lead to tests that are fragile and overly

complex. Focus on the key external dependencies that affect the unit of work being
tested.

46 - Explain Asymmetric and Symmetric encryption in iOS

Purpose: Evaluates candidate’s knowledge of cryptographic principles by


distinguishing between asymmetric and symmetric encryption methods, including
their uses and key management strategies.

Encryption is a fundamental concept in securing data and communication in iOS apps. It


involves converting plain text or data into a coded form that can only be deciphered with

GrokkingSwift 113
the correct decryption key. iOS provides various cryptographic APIs and libraries to
implement encryption in app.

Let's dive into the two main types of encryption: asymmetric and symmetric.

Symmetric Encryption

Symmetric encryption, also known as secret-key encryption, uses the same key to encrypt

and decrypt data. The key must be shared between the sender and the receiver before the
encryption and decryption can take place.

Symmetric encryption is faster and simpler than asymmetric encryption, but it is less
secure. It is suitable for large amounts of data, such as transferring files or streaming
media.

Swift doesn't have built-in symmetric encryption in the standard library, but you can use
the CommonCrypto library (not directly accessible in Swift, requires a bridging header for

Objective-C) or the newer CryptoKit framework available from iOS 13.

Asymmetric encryption

Asymmetric encryption, also known as public-key encryption, uses two different keys: a

public key for encryption and a private key for decryption. The public key can be shared
with anyone, while the private key must be kept secret. This way, only the holder of the
private key can access the encrypted data. Asymmetric encryption is more secure than

symmetric encryption, but it is also slower and more complex. It is suitable for small
amounts of data, such as exchanging keys or signing messages. In iOS, you can use the
Security framework and the SecKey API to perform asymmetric encryption and decryption

operations. You can also utilize CryptoKit if you app supports iOS 13 or newer.

GrokkingSwift 114
In iOS development, choosing between symmetric and asymmetric encryption depends on

your specific security requirements, data size, and performance considerations. For end-to-
end secure communications, a common pattern is to use asymmetric encryption to
exchange a symmetric key securely and then use symmetric encryption for the actual

message data, combining the strengths of both methods.

Remember, encryption is just one aspect of securing data in iOS apps. It should be used in

combination with other security measures, such as secure communication protocols,


secure storage, and proper authentication and authorization mechanisms.

47 - Explain state preservation and restoration in iOS.

Purpose: Looks at the candidate's experience with preserving and restoring app
state, a key aspect of creating a seamless user experience, especially in apps that
handle significant amounts of data or have complex interfaces.

In iOS, state preservation and restoration is a process that allows apps to save their
current state when they are terminated (either by the system or the user) and restore that
state when they are relaunched. This ensures a seamless user experience, as users can

return to the app in the same state they left it, without losing their progress or having to
navigate back to where they were. Implementing state restoration can significantly

enhance the usability of an app, especially for complex apps with multiple navigation
paths and states.

State preservation and restoration involve several key steps and components:

First, you must opt-in for state preservation and restoration by implementing specific
methods in your app delegate. This includes setting a restoration identifier for each

GrokkingSwift 115
view controller that needs to be preserved. This restoration identifier is used by the
system to uniquely identify the object during the save and restore process.
When the app moves to the background or is about to terminate, iOS asks the app to

save the state of its user interface by calling the encodeRestorableStateWithCoder:


method on your view controllers. Here, you serialize the necessary information about
your view controller's state using an NSCoder object.
Upon app restart, if a saved state is present, iOS attempts to restore the app’s UI to its
previous condition. It instantiates the app's view controllers using their restoration
identifiers and calls decodeRestorableStateWithCoder: on them. In this method, you

deserialize the information previously saved and apply it to restore the view
controller's state.
iOS automatically saves and restores the view hierarchy of your app based on the
restoration identifiers of your view controllers. However, you are responsible for saving
and restoring the data models or other state information your view controllers need.

Best Practices for State Preservation and Restoration

Only save the state that is necessary to restore the app to its current view or context.
Avoid saving sensitive information or large data sets that can be easily reloaded or
recreated.
Ensure that restoration identifiers are consistent across app launches. Changing
identifiers can prevent the app from correctly restoring its state.

Thoroughly test state restoration by simulating conditions that cause your app to be
purged from memory, such as using the Simulate Memory Warning option in Xcode or
terminating the app from the multitasking UI.
Your app should restore to a usable state even if the exact restoration state cannot be
achieved. For example, if the data needed for restoration is no longer available, the

GrokkingSwift 116
app should handle this gracefully, possibly by navigating to a default state.

48 - Discuss the role of NotificationCenter in an iOS app.

Purpose: Tests the candidate's knowledge of using NotificationCenter for decoupled


communication in an app, including its advantages and potential pitfalls, such as
debugging challenges and memory management.

NotificationCenter allows one-to-many communication between objects, where an object


(the publisher) broadcasts a notification to multiple objects (the subscribers) who have
registered for that notification. The subscribers can then respond to the notification

accordingly.

Apart from app-specific events, NotificationCenter is also used to listen for system
notifications, such as keyboard appearance, app lifecycle events, or changes in device
orientation.

The Advantages

NotificationCenter allows various components of an application to communicate with


each other without having direct references. This is beneficial in reducing
dependencies between components, making the code more modular and easier to
maintain.
Notifications can be posted and observed from any part of the application. In addition,
multiple observers can register for the same notification. This flexibility makes it easy

to implement broadcast communication.

Let’s look at an example:

GrokkingSwift 117
Consider an app with multiple components that need to adjust their state based on the
user's authentication status. For instance, some views may need to display or hide content
based on whether the user is logged in or not.

Without NotificationCenter, each component would need to individually check the


user's authentication status, possibly leading to duplicated code, or you'd need a more

complex system of delegates or callbacks to notify components of changes.


With NotificationCenter, you can broadcast a notification when the user's
authentication status changes, and all interested parts of the app can adjust
accordingly.

The Potential Pitfalls

Notifications are identified by string names, which can be prone to typos and lack

compile-time type safety.


As the number of notifications grows, tracing the flow of notifications and debugging
issues related to notifications can be challenging.
Improper use of notifications can lead to performance issues if observers do heavy
work in response to frequent notifications.
Passing data through userInfo doesn’t enforce a contract and requires casting, which

can lead to runtime errors if the data is not as expected.

To mitigate these trade-offs, you can apply these practices:

Use well-defined and descriptive notification names to avoid naming conflicts and
improve readability.

Be selective in using notifications and consider alternative communication patters such


as delegation, closures when appropriate.

GrokkingSwift 118
Keep the userInfo lightweight and only include necessary data to minimize
performance impact.
Properly remove observers when they are no longer needed to prevent memory leaks
and unnecessary overhead.

Overall, NotificationCenter is a useful tool for communication between different parts of


an application without tightly coupling those components. While it offers flexibility and
decoupling, you must be mindful of its trade-offs, such as potential debugging challenges
and the need for diligent observer management. Properly used, it can significantly
enhance the modularity and maintainability of an application.

49 - How do you approach internationalization and


localization in iOS apps?

Purpose: Evaluates the candidate's experience with making apps accessible to a


worldwide audience, covering technical aspects such as string localization, layout
adaptation, and cultural considerations.

Internationalization

Internationalization is the process of designing and building your app to support various
languages and regions without requiring engineering changes to the source code for each
locale. To internationalize your app, you need to:

Enable base internationalization in Xcode. This will separate the user-visible text and
images from the executable code, and store them in separate resource files within the

app bundle.

GrokkingSwift 119
Use Auto Layout to create UI layouts that can adjust to different screen sizes,
orientations, and languages. Avoid using hard-coded coordinates, sizes, or alignments

for your UI elements.


Use NSLocalizedString and related macros to mark the user-visible text in your code
for localization. This will allow you to provide translations for the text in different
languages, without changing the code logic.
Use Foundation APIs such as DateFormatter , NumberFormatter , and
MeasurementFormatter to format and display the variations in date and time formats,

number separators, currency symbols, and measurement units, according to the user’s
locale preferences.
If your app will be available in languages that read right-to-left, ensure your UI layout
and navigation can adapt accordingly by using semantic content attributes.
Use Unicode for character encoding to support a wide range of characters from
different languages.

Localization

Localization involves translating your app’s content and adapting it for specific languages
or regions. To localize your app, you need to:

Localize resources such as strings, images, and storyboards. Xcode helps manage
localized resources by grouping them under their respective language.lproj folders.

Localize your app’s name, description, keywords, and screenshots in the App Store to
improve discoverability and appeal to users in different markets. You can also use the
App Store pricing matrix to set the appropriate prices for your app and in-app
purchases in different currencies.
Be mindful of cultural differences that might affect your app's design, such as colors,
icons, and customs

GrokkingSwift 120
Perform locale-specific testing to see how your app handles different scenarios such as

long text or bidirectional text.

Localization is not a one-time task but a continuous process, especially if you regularly
update your app or add new features. Consider integrating localization into your
development workflow and possibly using translation management platforms or services
to streamline the process.

Above are the steps involved in preparing an app for a global audience. By following these
steps, you can make your app more user-friendly, engaging, and successful in the
international market

50 - Describe the process of submitting an app to the App

Store.

Purpose: Assesses the candidate's familiarity with the end-to-end process of


preparing and submitting an app for App Store review, highlighting their
understanding of distribution certificates, provisioning profiles, and app metadata
requirements.

Submitting an app to the App Store involves several key stages: code signing, app

provisioning, configuring your app in App Store Connect, and finally submitting your app
for review.

1. App Provisioning and Code Signing

To develop and distribute an application, you first need to deal with app provisioning and
code signing. You typically let Xcode handle the heavy lifting here as it manages your

GrokkingSwift 121
provisioning profiles and signing certificates automatically. You just need to make sure
you’re logged into your Apple developer account in Xcode and check Automatically
Manage Signing under Signing & Capabilities tab in your project’s target settings.

While Xcode can manage the provisioning profiles automatically, you’re free to handle

them manually. Just make sure you select the appropriate type of profile. Use
Development Profile for testing your app on real devices during development. When you’re
ready to distribute your app via the App Store, use the Distribution Profile.

2. Prepare Your App for Submission

The next step is to prepare all required app icon sizes and a launch screen for your app.
It's not just about the app's functionality, it's how you present it to attract the users. Make

sure you have used Instruments and other tools to optimize your app’s performance,
ensuring it’s fast and responsive. You can consider using TestFlight to conduct thorough
testing for your app, including all the iOS versions and devices you support.

3. Configure Your App in App Store Connect

Before you can upload your app, you need to configure it in App Store Connect by filling in
your app details such as the name of your app, description, bundle ID, keywords, support

URLs, and privacy policy URL, etc. From here, you are also required to upload screenshots
for all device sizes you support, following the specific requirements for screen dimensions
and resolutions. Then, decide on your app's pricing and select the countries in which it will
be available.

4. Upload Your App to App Store Connect

GrokkingSwift 122
You can archive and upload the app to App Store Connect using Xcode or the Transporter
app. Make sure the app’s version number and build number are correct and match the ones
on App Store Connect.

5. Submit for Review

Once your app and its metadata are ready in App Store Connect, click "Submit for Review."
You’ll need to answer questions about your app’s content, provide contact information, and
confirm export compliance.

6. Handling Rejections

If your app is rejected, carefully review the feedback provided by the App Store Review

team, make the necessary adjustments, and resubmit. Common reasons for rejection
include unresolved bugs, privacy concerns, lack of valuable content, or non-compliance
with the App Store Review Guidelines.

GrokkingSwift 123

You might also like