50 Essential IOS Interview Questions and Answers
50 Essential IOS Interview Questions and Answers
50 Essential IOS Interview Questions and Answers
50 Interview
iOS
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
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.
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.
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
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
initializer if you don’t define any custom initializers. Memberwise initializer is not
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
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
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.
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
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
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
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.
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
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.
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
}
}
updateUIWithData() method.
The delegate property is of a fixed type ViewController, which means it can only
compile-time safety. The delegating object doesn’t know if the delegate actually
Using protocols for delegation addresses the drawbacks mentioned above and provides
several advantages:
GrokkingSwift 8
protocols for delegation, it ensures that the delegate conforms to the expected
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,
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
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.
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.
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:
In OOP, classes are the primary building blocks. Classes define the structure and
In POP, protocols are the primary building blocks. They define a set of requirements
OOP relies heavily on inheritance, where subclasses inherit properties and behaviors
which can lead to tightly coupled code where changes to the superclass can have
properties, and other requirements, without implementing them. Types can adopt
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
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,
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.
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
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
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.
Now we can use the swapValues functions with different parameter types, as long as they
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"
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
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
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.
Without using generics, we would need to define separate structs for each combination of
struct IntStringPair {
var first: Int
var second: String
struct BoolDoublePair {
var first: Bool
var second: Double
GrokkingSwift 14
To create instances of these pairs, we would do the following:
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
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
You can also use where clauses to specify more complex constraints, including
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.
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.
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.
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
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
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
Swift provides automatic bridging between Array and NSArray. You can use them
interchangeably in many cases, but with some considerations due to copying.
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
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
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.
@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
inlining or tail call elimination, that can improve the performance and memory usage
of the code. @escaping closures prevent these optimizations, because the compiler
GrokkingSwift 21
8 - Explain the concept of optionals in Swift. How do they
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.
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.
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.
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
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.
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.
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
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.
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.
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
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
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.
GrokkingSwift 26
Purpose: Evaluates the candidate’s knowledge of inheritance within the context of
Swift’s type property features, including their accessibility, override capabilities,
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).")
}
}
Game.maxPlayers = 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
class Game {
class var maxPlayers: Int {
return 4
}
}
print(Game.maxPlayers) // Outputs: 4
print(BoardGame.maxPlayers) // Outputs: 2
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
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
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?
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
However, updating a static library requires recompiling the app. Different versions of the
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.
GrokkingSwift 30
Aspect Static Libraries Dynamic Libraries
Can be updated
Versioning/Updating Requires recompiling the app
independently
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
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?
focusing on its role in providing location data and best practices for its effective
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:
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.
Now that we’ve explored the features of Core Location, let’s delve into some key strategies
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
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.
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
Monitor battery consumption and network usage related to location services. Implement
5. Handle Errors
Deal with scenarios like location unavailable, authorization denied, or network issues
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
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
There are several existing techniques to draw a circle without directly using UIBezierPath.
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.
You can draw a circle by overriding the draw() method of a UIView and using Core
Graphics functions. Here is an example.
GrokkingSwift 34
context.setLineWidth(lineWidth)
2. Using CAShapeLayer
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
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)
}
}
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
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.
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
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.
Purpose: Tests the candidate's proficiency in creating flexible and adaptive user
interfaces using Auto Layout, Size Classes, and UIKit, ensuring a seamless user
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
https://github.com/layoutBox/LayoutFrameworkBenchmark.
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
UIStackView simplifies the process of designing adaptable UIs with Auto Layout. Stack
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
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
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
Most importantly, testing! You can use either simulators or real devices to verify that your
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.
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
This is the initial state, where the UIViewController instance hasn’t been allocated in
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.
viewWillAppear() is called before the view is added to the view hierarchy, indicating
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
GrokkingSwift 41
or the view is about to be hidden. This is where you should stop animations, cleaning
the window.
Usually under low-memory conditions, the view controller’s view will be released from
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
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
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
You can reload a UITableView either completely using reloadData() or partially using
GrokkingSwift 43
UI frameworks are not thread-safe. Performing UI updates on a background thread can
DispatchQueue.main.async {
self.tableView.reloadData()
}
let indexPathsToReload = [
IndexPath(row: 0, section: 0),
IndexPath(row: 1, section: 0)
]
DispatchQueue.main.async {
self.tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}
masking.
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,
In this example, clipsToBounds ensures that the image is clipped to fit within the rounded
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
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
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
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
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
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.
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.
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.
view’s layout properties (e.g., frame, bounds, constraints) have changed and that the
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.
func updateUserInterface() {
// Modify the frames of subviews
view1.frame = ... // new frame for view1
view2.frame = ... // new frame for view2
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
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:
func animateViewExpansion() {
// Change the frame of the view (without animation)
expandableView.frame = ... // new, larger frame
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,
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:
layoutIfNeeded()
Changes to the size of a view’s bounds (like when rotating the device, changing the
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?
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:
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.
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
GrokkingSwift 53
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
color scheme from the environment. The view can then use this value to conditionally
If you wish to add your custom values to the environment, you need to create an
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:
@EnvironmentObject
The @EnvironmentObject property wrapper is used to share data objects instance across
ObservableObject protocol.
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
GrokkingSwift 55
Next, suppose we will inject this view model from a ParentView, then access and modify it
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.
GrokkingSwift 56
This ChildView can access the shared instance of MyViewModel using the syntax of
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
potential bottlenecks in view rendering, data processing, and memory management. Here
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
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.
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
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.
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.
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
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.
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?
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
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.
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.
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")
.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
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.
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
Common Pitfalls
Despite its efficiency in managing memory, using ARC comes with pitfalls that you need to
be aware of:
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.
Unlike weak references, unowned references are expected to always have a value.
GrokkingSwift 64
access that reference can lead to a runtime crash. It’s important to ensure that unowned
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
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.
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
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.
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
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.
Purpose: This question tests the candidate's ability to design and implement real-
For real-time data, traditional HTTP requests (RESTful services) might not be efficient
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
4. User Experience
Use placeholders or loading indicators for data that is expected but not yet received to
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.
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.
supports.
URLSession is a powerful tool for handling HTTP requests and managing network tasks. It
1. Shared Session
quickly access a shared session without the need to create and manage a separate session
instance.
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
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.
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.
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
cache policy, timeout values, HTTP headers, network service type, and more. Here’s an
example:
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
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.
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,
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.
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
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,
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
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
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
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
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.
Purpose: Assesses the candidate's knowledge of data modeling within Core Data
and their ability to handle complex relationships efficiently.
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
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
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
When you create a new task, you insert it into the context. Until you save the context,
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
Core Data typically uses one or more MOCs, which can have different types
GrokkingSwift 77
contexts for UI updates and background processing. However, keeping them in sync can be
challenging.
in iOS?
behavior, crashes, or corrupted data. Swift and iOS provide several mechanisms to manage
access to shared resources and ensure thread safety effectively.
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
func updateSharedResource() {
serialQueue.async {
// Code to update shared resource safely
GrokkingSwift 78
}
}
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.
func readSharedResource() {
concurrentQueue.async {
// Code to read shared resource
}
}
func writeSharedResource() {
concurrentQueue.async(flags: .barrier) {
// Code to write shared resource safely
}
}
3. Synchronization Primitives
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.
func updateSharedResource() {
lock.lock()
// Code to update shared resource safely
lock.unlock()
}
4. Operation Queues
These are some of the ways to ensure thread safety in iOS. However, they may have some
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.
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.
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
class Cache {
private var data = [String: Any]()
private let queue = DispatchQueue(label:
"io.grokkingswift.cacheQueue", attributes: .concurrent)
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.
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.notify(queue: .main) {
// All requests have completed; execute completion handler
print("All network requests have finished.")
}
GrokkingSwift 84
2. OperationQueues with Dependencies
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.
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.
GrokkingSwift 85
3. Using Async/Await with Task Groups
operations using a TaskGroup. Here’s how you can perform multiple network requests in
parallel and wait for all to complete:
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
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.
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
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.
DispatchQueue.global(qos: .background).async {
// Perform your background code here
}
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.
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
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:
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
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
Purpose: Tests the candidate's ability to diagnose and solve common performance
drawing operations. There are several strategies that you can apply to improve the
performance:
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.
GrokkingSwift 91
2. Reuse Cells
Ensure that cells are reused efficiently by dequeuing them properly using
dequeueReusableCell(withIdentifier:) for UITableView or
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
cell content.
4. Image Handling
Scale down large images to the size they will be displayed at before showing them in
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
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.
improve the scrolling performance of UITableView and UICollectionView in your iOS app,
ensuring a smooth and responsive user experience.
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
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
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
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.
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.
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
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
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.
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
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
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.
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.
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
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() {}
Singletons are prevalent in iOS development. They're used to provide a controlled access
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.
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
Purpose: Assesses the candidate's grasp of one of the foundational design patterns
GrokkingSwift 100
pattern facilitates communication between objects in a way that reduces coupling and
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
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
the delegated responsibilities and promotes reusability of both the delegator and
delegate objects.
GrokkingSwift 101
Alternative: Closure-based Callbacks
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.
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.
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
including the ability to implement MVVM in Swift, the benefits of using it over
MVC, and understanding of data binding mechanisms.
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
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
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
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
GrokkingSwift 106
Example of a throwable function:
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.
GrokkingSwift 107
}
do {
try processFiles(filePaths: ["/path/to/file1", "/path/to/file2"],
fileProcessor: readFile)
} catch {
print("An error occurred: \(error)")
}
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
UIViewController.
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
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
Define protocols for your external dependencies and make your controller depend on
these protocols rather than concrete implementations. Then, create mock
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.
Purpose: Checks the candidate's proficiency in writing testable code and their
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
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.
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
GrokkingSwift 111
}
// Test Case
class NetworkTests: XCTestCase {
func testFetchUserDetailsSuccess() {
let mockService = MockNetworkService()
let viewModel = ViewModel(networkService: mockService)
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
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
GrokkingSwift 112
latency, intermittent failures, and unexpected server response changes aren't accounted
important.
Best Practices
When designing your classes and services, keep dependency injection in mind. It makes
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.
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
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
Remember, encryption is just one aspect of securing data in iOS apps. It should be used in
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
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.
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.
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
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.
Notifications are identified by string names, which can be prone to typos and lack
Use well-defined and descriptive notification names to avoid naming conflicts and
improve readability.
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.
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
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
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
Store.
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.
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.
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.
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.
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.
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