Top iOS Interview Questions and Answers 2024
Sep 14, 2024As an iOS developer, a lot of concepts revolve in the headspace when it comes to iOS interview preparation. But top industry trends are shifting and getting success in an iOS interview is not solely dependent on what concepts you understand but how much in-depth and hands-on knowledge you have of those concepts.
In this article, you'll learn 20 most frequently asked iOS Interview Questions with Answers. These questions have been asked in top tech companies like MakeMyTrip, Zomato, Gojek, Swiggy, Paytm etc.
Table of Contents
- What is the difference between Static and Class variable?
- Are lazy vars computed more than once?
- What is the difference between self and Self?
- How to create Optional methods in Protocol?
- How does memory usage optimization happen in UITableView?
- What is Dependency Injection and what are it's advantages?
- Explain the difference between throws and rethrows in Swift.
- Explain the types of sessions and tasks supported by URLSession class.
- Compare static and dynamic libraries.
- How would you implement an Infinite Scrolling List?
- What is Copy-on-Write in Swift? Explain how to customize its implementation.
- Explain Factory design pattern usage and what problem does this pattern solve.
- What is APNS and how does it work?
- Explain the purpose of the 'mutating' keyword in Swift.
- What is the difference between @StateObject and @ObservedObject in SwiftUI?
- What are Mocks and Stubs and what is the difference between them?
- Explain Equatable, Hashable, and Comparable protocol in Swift.
- What does UIApplicationMain mean?
- Explain how Swift is a type-safe language?
- Why do you need escaping closures and when should you use them?
What is the difference between Static and Class variable?
Static and Class variables are both similar in nature, they are used to create properties that belongs to the type(Class/Struct/Enum) not it's instances. They have one major difference i.e. Inheritance. Let's learn more about it.
Static Variable:
A static variable is defined using the static
keyword and is associated with a specific type. This means that there is only one instance of the variable across all instances of the type, and it can be accessed using the type name instead of an instance of the type. For example:
struct Vehicle {
static var wheels: Int {
return 0
}
}
print(Vehicle.wheels) // 0
Class Variable:
A class variable is defined using the class
keyword and is also associated with a specific type like a static variable. However, unlike a static variable, a class variable can be overridden by subclasses. For example:
class Vehicle {
class var wheels: Int {
return 0
}
}
class Car: Vehicle {
override class var wheels: Int {
return 4
}
}
class Bike: Vehicle {
override class var wheels: Int {
return 2
}
}
print(Vehicle.wheels) // 0
print(Car.wheels) // 4
print(Bike.wheels) // 2
Are lazy vars computed more than once?
In most cases, Lazy variable is initialised only when it is accessed for the first time. The initialisation code is executed only once, and the result is stored in the variable. Subsequent accesses to the variable return the stored value, without recomputing it.
Here is an example:
class Student {
lazy var defaultMark: Int = {
print("Computing lazy variable defaultMark")
return 30
}()
}
let student = Student()
print(student.defaultMark)
print(student.defaultMark)
Here is the output:
Computing lazy variable defaultMark
30
30
In this example, defaultMark
is computed only once, when it is first accessed. The print statement inside the closure is executed only once, and subsequent accesses to defaultMark
return the stored value without recomputing it.
💡Important Note: Lazy variables are not thread-safe by default, which means that if a lazy property is not yet initialized and multiple threads try to access it, there is a possibility that it will be initialized more than once. However, if explicit thread-safe practices are implemented on the lazy property, such cases can be avoided.
Here are some important points to remember about lazy variables in Swift:
- The lazy variable must be declared as a variable with the
lazy
keyword written before it. - The lazy variable's type must be explicitly declared, or it must be inferable from the initialisation closure.
- After the lazy variable has been computed, its value is stored, so subsequent accesses to the variable return the stored value without recomputing it.
- Lazy variables are useful when the initialisation code is expensive or time-consuming, and you want to avoid unnecessary computation. However, they should be used with caution, as they can add complexity and make the code harder to understand.
- Lazy variables are not suitable for cases where the initialisation code has side effects or modifies state, as the side effects or state changes will occur only once, when the variable is first accessed.
What is the difference between self and Self?
In Swift, the difference between self
and Self
lies in their usage and context. They are used for different purposes and represent different things:
self:
The self
is used to refer to the current instance of a class, structure, or enumeration within its own instance methods or initializers. It is similar to this
keyword in other programming languages. For example:
class MatchScore {
let value: Int
init(value: Int) {
self.value = value
}
}
let cricketScore = MatchScore(value: 325)
print(cricketScore.value) // print: 325
In the above example, we define an initializer init(value:)
with an argument named value
of type Integer. You can see that the initializer parameter and the class property have the same name (i.e. value). To assign value to the stored property value
, we use self.value
.
Self:
The Self
is used to refer to the type of the current class, structure, or enumeration within its own methods or initializers. It is similar to this
or typeof
in other programming languages.
For example:
protocol ScoreCreation {
static func create() -> Self
}
class MatchScore: ScoreCreation {
required init() { }
static func create() -> Self {
return self.init()
}
}
let scoreObject = MatchScore.create()
print(type(of: scoreObject)) // print: "MatchScore"
In this example, MatchScore
conforms to the ScoreCreation
protocol, which implements a create()
method that returns Self
, which is the instance of the current class.
Finally, the type(of:)
function is used to print the type of the returned instance, which is MatchScore
.
How to create Optional methods in Protocol?
To create optional methods in a protocol, Swift has two approaches. Each approach has its own advantages and disadvantages. Let's understand them with an example.
1. Use the optional keyword:
You can create optional methods in a protocol by using the optional
keyword before the method declaration. The protocol can then be adopted by a class and the method does not need to be necessarily implemented.
Here's an example:
import UIKit
@objc protocol CountryPickerDelegate {
// required method
func didCountrySelected(at index: Int)
// optional method
@objc optional func didCountryPickerDismiss()
}
class RegisterController: UIViewController, CountryPickerDelegate {
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - CountryPickerDelegate
func didCountrySelected(at index: Int) {
// write implementation here..
}
}
Note: This approach can be used only for classes that inherit from NSObject. That means you cannot conform structs or enums to the protocol.
2. Use the Extension:
By providing an empty implementation of methods in a protocol extension, you can make them optional.
Here's an example:
protocol CountryPickerDelegate {
// required method
func didCountrySelected(at index: Int)
// need to be optional
func didCountryPickerDismiss()
}
extension CountryPickerDelegate {
// empty implementation
func didCountryPickerDismiss() { }
}
Note: This approach can be used with all the types conforming to this protocol.
🪄Anytime Magic Tip:
By default, all the methods of a protocol are required. It is always recommended to make optional methods in order to avoid necessary implementation of all the methods even when it's not required.
How does memory usage optimization happen in UITableView?
When you create a UITableView
, and have a large number of cells to display, it might take huge memory as tableview needs to allocate memory for each cell and if the memory usage goes beyond a certain threshold then the app will lag or crash.
For optimizing memory usage you can implement UITableView's dequeueReusableCell(withIdentifier:for:)
method which returns a cell with the specified reuse identifier. UITableView
uses a Queue data structure to solve the memory usage problem. When you scroll through a table view, all the cells which are moving out of the visible area are stored in queue. When they are being displayed again cellForRowAt
method will be called and the content of that particular cell will be displayed.
There is catch when content for a particular cell is displayed, sometimes content that is not related to cell is also visible. Apple has recommended using the prepareForReuse
method to reset or clean-up the cell content so that content not related to a particular cell is not visible.
When you use this method, you must provide a reuse identifier that corresponds to the identifier you set when you created the prototype cell in your storyboard, XIB file, or programmatically.
Here's an example usage of dequeueReusableCell(withIdentifier:for:)
:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyTableViewCell
// configure the cell with data
return cell
}
What is Dependency Injection and what are it's advantages?
Dependency Injection is a design pattern that promotes loosely coupled code by separating the creation and management of dependencies from the class that uses them. Dependency Injection can be implemented in several ways.
Constructor injection:
This involves passing dependencies into a class's init. For example, if a class depends on a network client, you can inject the client into the class's init:
class ViewController: UIViewController {
let networkClient: NetworkClient
init(networkClient: NetworkClient) {
self.networkClient = networkClient
super.init(nibName: nil, bundle: nil)
}
// ...
}
Property injection:
This involves passing dependencies through properties. Note that Property injection requires the dependency to be optional, which can make the code more error-prone. For example:
class ViewController: UIViewController {
var networkClient: NetworkClient?
// ...
}
Method injection:
This involves passing dependencies into a method when it's called. For example:
class ViewController: UIViewController {
func fetchData(using networkClient: NetworkClient) {
// ...
}
// ...
}
Advantages of Dependency Injection :
Here are some advantages of implementing dependency injection in your project:
Testability: When a class has dependencies that are tightly coupled, it can be difficult to test the class in isolation. By injecting dependencies through the constructor, you can easily swap out the dependencies with mock objects or stubs for testing purposes.
Flexibility: Dependency injection can make your code more flexible and modular. By injecting dependencies, you can easily switch out one implementation of a dependency for another, without having to modify the class that uses it.
Separation of concerns: By separating the creation and management of dependencies from the class that uses them, you can achieve a better separation of concerns in your code. This can make your code easier to understand, modifiable, and extendable over time.
Reusability: By injecting dependencies, you can make your classes more reusable in different contexts. For example, you might have a network client that is used in several different classes throughout your app.
Explain the difference between throws and rethrows in Swift.
In Swift, throws
and rethrows
are used to indicate that a function can potentially propagate an error within it to its caller.
throws:
The throws
keyword is used to mark a function that can throw an error. When a function is marked with 'throws', it means that it can potentially generate an error during its execution. In order to handle such errors, callers of the function must either use a 'do-catch' block to handle the error or propagate the error to their own caller using the 'throws' keyword.
enum MyError: Error {
case invalidInput
}
func divide(_ Answer: Int, by b: Int) throws -> Int {
guard b != 0 else {
throw MyError.invalidInput
}
return a / b
}
do {
let result = try divide(10, by: 0)
print(result)
} catch let error {
print(error)
}
// print: invalidInput
In this example, the divide
function takes two integers as input and returns the result of dividing the first integer by the second integer. However, the function can potentially throw an error if the second integer is 0. Therefore, the function is marked with throws
.
rethrows:
The rethrows
keyword is used to mark a function that takes one or more throwing functions as parameters, and itself can propagate an error. When a function is marked with rethrows
, it means that it will only throw an error if one of its throwing function parameters throws an error. If none of the function's throwing parameters throw an error, the function will not throw an error itself. In other words, a 'rethrows' function rethrows an error that was thrown by one of its throwing function parameters.
enum MyError: Error {
case invalidInput
}
func manipulateArray(_ array: [Int], using closure: (Int) throws -> Int) rethrows -> [Int] {
var result: [Int] = []
for element in array {
let transformed = try closure(element)
result.append(transformed)
}
return result
}
func addOne(_ number: Int) throws -> Int {
guard number >= 0 else {
throw MyError.invalidInput
}
return number + 1
}
let input = [1, 2, 3, 4, 5]
do {
let result = try manipulateArray(input, using: addOne)
print(result)
} catch {
print("An error occurred: \(error)")
}
// print: [2, 3, 4, 5, 6]
In this example, the addOne
function is a throwing function that takes an integer as input and adds 1 to it. However, it throws an error if the input integer is negative. The manipulateArray
function is called with the addOne
closure as a parameter. The result is an array with each element incremented by 1. If any errors are thrown by the addOne
closure, they are rethrown by the manipulateArray
function and caught by the 'do-catch' block.
Explain the types of sessions and tasks supported by URLSession class.
URLSession is a powerful networking API in iOS and macOS that provides a set of classes and methods to perform various networking tasks, such as fetching data from a remote server or uploading data to a server.
There are three types of sessions supported by URLSession:
Default Session:
This is the most common and default session type used in the URLSession class. A global cache, credential storage object, and cookie storage object are used.
This is how we define the default session:
let config = URLSessionConfiguration.default
Ephemeral Session:
This session is similar to the default session but it doesn't use any storage like caches, cookies, or credentials. It's useful when you need to make requests with different or temporary credentials, like when you're authenticating with an API. It's also useful when you want to keep your requests and responses private and not stored on disk.
This is how we define the default session:
let config = URLSessionConfiguration.ephemeral
Background Session:
This session type is used when you want to download or upload data in the background, even when your app is not running. The system handles the session and provides progress updates through delegate methods. The background session is useful when you want to download large files or perform a lengthy operation that requires more than a few seconds.
This is how we define the default session:
let config = URLSessionConfiguration.background(withIdentifier: "com.example.background")
There are several tasks supported by URLSession:
Data Task: This task is used to retrieve data from a URL. It returns the server's response as Data in the completion handler. Here is the method syntax:
session.dataTask(with: <URLRequest>, completionHandler: <(Data?, URLResponse?, Error?) -> Void>)
Download Task: This task is used to download a file from a URL. It returns the downloaded file's location on disk in the completion handler. Here is the method syntax:
session.downloadTask(with: <URLRequest>, completionHandler: <(URL?, URLResponse?, Error?) -> Void>)
Upload Task: This task is used to upload data to a URL. It can upload data in the form of a file or a stream. Here is the method syntax:
session.uploadTask(with: <URLRequest>, from: <Data?>, completionHandler: <(Data?, URLResponse?, Error?) -> Void>)
WebSocket Task: This task is used to establish a WebSocket connection to a URL. It allows bidirectional communication between the client and server in real time. Here is the method syntax:
session.webSocketTask(with: <URL>)
Each of these tasks has its own set of delegate methods that can be used to monitor the progress of the task, handle authentication, handle errors, and more.
Note:
- There's no way to configure or customize the caching behaviour beyond setting the cache policy and timeout interval.
- While URLSession provides the ability to perform network requests in the background, background tasks are limited to 30 seconds by default.
- URLSession does not support all possible networking protocols like FTP or SMTP.
- There's no built-in way to retry failed requests automatically.
Compare static and dynamic libraries.
Static Library
When you compile the code, a unit of code is linked, which does not change. That piece of code is linked to the generated executable file by a static link during the compilation process. Static libraries can't include media files such as images or assets, which means static libraries can only contain a unit of code.
-
Compilation: Static libraries are compiled and linked directly into the executable binary at compile time.
-
Size: Static libraries increase the size of the final executable because the library code is duplicated in each binary that uses it.
-
Independence: Static libraries make the binary self-contained, as all the necessary code is included within the executable.
-
Performance:Static libraries offer better performance during runtime since the code is directly linked and doesn't require dynamic loading at runtime.
-
Update: If a change is made to a static library, all the applications using that library need to be recompiled and redeployed to incorporate the changes.
Dynamic Library
At runtime, dynamic libraries link units of code or media files that may change. They are different from static libraries in the sense that they are linked to the app’s executable at runtime, but not copied into it. In the case of a dynamic library, the executable file is smaller because the code is loaded only when it is needed at runtime.
-
Compilation: Dynamic libraries are compiled separately and linked at runtime when the application is launched.
-
Size: Dynamic libraries don't increase the size of the final executable as they are shared among multiple applications, reducing redundancy.
-
Dependency: Dynamic libraries can have dependencies on other libraries and frameworks, and they can be loaded and unloaded dynamically at runtime.
-
Performance: Dynamic libraries may have a slight performance overhead during runtime due to the dynamic loading and symbol resolution process.
-
Update: If a change is made to a dynamic library, all the applications using that library can benefit from the changes without recompilation or redeployment.
Dynamic libraries are commonly used for system frameworks provided by Apple, while static libraries are used for third-party libraries or when developers want to bundle specific functionality directly into their application. The choice between static and dynamic libraries depends on factors such as code size, performance requirements, update flexibility, and dependency management.
Some Related Questions :
- What are the advantages of using static libraries in iOS development?
- How are dynamic libraries different from static libraries in terms of memory usage?
- How are static libraries fast for app launch time?
- When would you choose to use dynamic libraries instead of static libraries in an iOS project?
How would you implement an Infinite Scrolling List?
To implement an infinite scrolling list in iOS, follow these steps:
- Set up a
UITableView
with a data source that provides data for the table view cells. - Implement the
UITableViewDelegate
methodscrollViewDidScroll
to detect when the user scrolls to the bottom of the table view. - When the user reaches the bottom of the table view, load more data and append it to the existing data source.
- Call
reloadData
on the table view to update the display with the new data.
Here is the basic code snippet to follow:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
// The user has scrolled to the bottom of the table view.
// Load more data here.
loadMoreData()
}
}
func loadMoreData() {
// Load more data here and append it to your existing data source.
let newData = fetchNextBatchOfData()
dataSource.append(contentsOf: newData)
// Reload the table view with the updated data source.
tableView.reloadData()
}
Counter Question :
How to improve scrolling performance of infinite List?
To improve infinite scrolling list's performance, you can follow these steps:
-
Load data incrementally: Instead of loading all data at once, load it in smaller batches to reduce memory overhead and improve performance.
-
Reuse table view cells: Use dequeueReusableCellWithIdentifier to reuse table view cells instead of creating new ones for each row. This reduces memory overhead and improves performance.
-
Cache images and other resources: If your table view contains images or other resources, cache them to reduce loading time and improve performance.
-
Use lazy loading: Load data only when needed, and not before. This approach can improve performance by reducing the amount of data loaded.
-
Avoid blocking the main thread: Loading data can be time-consuming, so it's important to avoid blocking the main thread. Use background threads to perform data loading and processing, and update the UI on the main thread.
What is Copy-on-Write in Swift? Explain how to customize its implementation.
Copy-on-Write is a memory optimization technique that allows multiple objects or variables to share the same data until one of them tries to modify it. When the modification begins, the data is copied to a new location, and the modification is performed on the new copy. This technique can significantly reduce memory usage and improve performance.
Copy-on-Write is used extensively by the system frameworks and APIs, especially in collections such as Array and Dictionary. Copy-on-Write is the magic behind value types. For starters, consider the following example:
var numbers = [10, 20, 30]
let numbersCopy = numbers
numbers.append(40)
print(numbers) // [10, 20, 30, 40]
print(numbersCopy) // [10, 20, 30]
As you probably know, collections like Arrays have Value semantics, it means that unlike Reference types (which store a reference to the object), Value types store a copy of the object. In the above example, numbersCopy gets a copy of numbers.
However, if you need to implement your own custom class that uses Copy-on-Write, you can do so like below example:
final class Person {
var name: String
init(name: String) {
self.name = name
}
}
struct PersonCOW {
private var person: Person
init(name: String) {
person = Person(name: name)
}
var name: String {
get {return person.name}
set {
if !isKnownUniquelyReferenced(&person) {
person = Person(name: newValue)
} else {
person.name = newValue
}
}
}
}
var person1 = PersonCOW(name: "John")
var person2 = person1
let names = ["Brian", "Stewie", "Peter"]
for name in names {
person1.name = name
}
print(person1.name) // Peter
print(person2.name) // John
Here: isKnownUniquelyReferenced
is a method provided by Swift's standard library that checks whether a given object is uniquely referenced in memory.
The name property’s getter will simply return the name, in that case, we don’t have to do anything complicated with it.
The magic happens in our setter. We first use our incantation isKnownUniquelyReferenced
to determine whether there are other objects pointing to the same memory location.
Few scenarios where Copy-on-Write may be particularly useful:
-
Large Data Sets: If you have a large amount of data that needs to be shared between multiple objects, copying the data every time it is accessed can be expensive in terms of both time and memory usage. Copy-on-Write can reduce the amount of memory needed to store data by sharing it between objects until one modifies it.
-
Immutable DatAnswer: If the data you share between objects is immutable, there is no risk of unintended modification. In this case, Copy-on-Write can avoid unnecessary copying and improve performance.
Few examples of where Copy-on-Write could be used in iOS:
-
Text editing app: If you're building a text editing app, you could use Copy-on-Write to optimize memory usage when multiple edits are made to the same piece of text. For example, if you have a large string shared between multiple views, you could use Copy-on-Write to ensure that the string is only copied when modified.
-
Graphics editing app: If you're building a graphics editing app, you could use Copy-on-Write to optimize memory usage when multiple objects are based on the same underlying data. For example, if you have a set of shapes based on the same path, you could use Copy-on-Write to ensure that the path is only copied when it's modified.
Explain Factory design pattern usage and what problem does this pattern solve.
The Factory design pattern is a creational pattern that provides an interface for constructing objects in a superclass. However, it allows subclasses to alter the type of objects created. This pattern solves the problem of creating objects without exposing the creation logic to the client. It decouples the client code from the specific classes being used.
Use of factory design pattern:
iOS development uses the Factory pattern for creating and managing different types of objects, such as views, controllers, or models. By using a Factory class to create these objects, the code becomes more flexible and easier to maintain.
A scenario to use it:
For example, imagine an app that displays different types of charts, such as pie charts, bar charts, and line charts. Instead of creating each type of chart directly in the view controller, a ChartFactory class can be used to create the different types of charts. This allows the creation logic to be encapsulated in one place, making it easier to change or add new types of chart in the future.
Additionally, the Factory pattern can also improve code testability by allowing easier dependency injection. Instead of directly instantiating objects, the Factory class can be injected as a dependency. This makes it easier to replace or mock creation logic during testing.
Here's an example of how the Factory pattern can be used in iOS development:
First, we create a ProductFactory protocol that defines a method for creating product view controllers:
protocol ProductFactory {
func createProductViewController(for product: Product) -> UIViewController
}
Next, we create a concrete implementation of the ProductFactory protocol for each type of product. For example, here's a BookProductFactory that creates book view controllers:
class BookProductFactory: ProductFactory {
func createProductViewController(for product: Product) -> UIViewController {
let book = product as! Book
let bookViewController = BookViewController()
bookViewController.book = book
return bookViewController
}
}
Finally, in the main view controller, we can use the ProductFactory to create the appropriate product view controller based on the selected product:
class MainViewController: UIViewController {
var productFactory: ProductFactory!
var selectedProduct: Product!
func showSelectedProduct() {
let productViewController = productFactory.createProductViewController(for: selectedProduct)
navigationController?.pushViewController(productViewController, animated: true)
}
}
By using the Factory pattern, we've encapsulated the creation logic for each type of product view controller in separate classes, making it easier to maintain and extend the code in the future.
What is APNS and how does it work?
APNS (Apple Push Notification Service) is a cloud service that allows the application's server to send notifications to the device when a new event triggers. It works by establishing a persistent connection between an iOS device and Apple's server.
When an app wants to send a notification, it sends the message to the APNS server, which then delivers the message to the target device. The device displays the notification, which can include text, sound, or an icon badge, to the user. APNS requires a secure connection and uses a combination of certificate-based authentication and encryption to ensure that only authorized applications can send notifications to iOS devices.
How APNS works:
- Enable push notification by asking user's permission to send notifications.
- Once a device registered for push notifications, application's server sends a push notification request to the APNS server, including the target device token and the notification payload.
- The APNS server receives the request and sends the notification to the target device over a persistent and secure connection.
- The device receives the notification and displays it to the user, either as an alert, a banner, or a sound, depending on the user's settings and the type of notification.
- If the user interacts with the notification, such as tapping it, the app associated with the notification launches and receives the user's response.
Note: The device token for one app can’t be used for another app, even when both apps are installed on the same device. Both apps must request their own unique device token and forward it to your provider's server.
The delivery of remote notifications involves several key components:
- Your application's server, known as the provider server
- Apple Push Notification service (APNs)
- The user’s device
- User has allowed notifications for your app on the device
Important: In your developer account, enable the push notification service for the App ID assigned to your project.
Counter Question:
What are silent/background push notifications and when are they implemented?
Silent/background push notifications are the notifications that wake up the app in background and doesn't show an alert, badge number or play a sound. The OS considers these notification as low priority task and it may restrain the delivery of these notifications if the count is excessive. As per Apple's recommendation, the frequency of these notifications should not be more than 2-3 notifications per hour.
When to use Silent/Background Notifications?
1. Background App Refresh: Silent push notifications can be used to call background refresh, allowing apps to update their content even when they are not actively running.
2. Geolocation: Some apps that use location based services can use these notifications to monitor a user's location in the background. For example, food delivery apps can send a personalised offer notification when user enters a particular location.
3. Content Synchronisation: Messaging apps can use silent notifications to fetch new messages or updates in the background. This keeps the app's content always updated even without app being opened by the user.
Some Related Questions:
- What is a device token, and how is it used in APNS?
- How does an iOS app handle push notifications when it is in the foreground?
- What are the different types of notifications that can be displayed on an iOS device?
- Can you describe the structure of a typical notification payload in APNS?
Explain the purpose of the 'mutating' keyword in Swift.
By default, the properties of Value types like Structs cannot be mutated by the methods defined within the scope of the Struct. The mutating
keyword is used as a prefix to methods that enables the mutation of instance properties.
For example, consider the following code without mutating
keyword:
struct Stack {
var numbers = [1, 2, 3]
func append(_ number: Int) {
numbers.append(number)
}
}
var intStack = Stack()
intStack.append(4)
print(intStack.numbers)
Output:
error: cannot use mutating member on immutable value: 'self' is immutable
numbers.append(number)
Consider the following code with mutating
keyword:
struct Stack {
var numbers = [1, 2, 3]
mutating func append(_ number: Int) {
numbers.append(number)
}
}
var intStack = Stack()
intStack.append(4)
print(intStack.numbers)
Output:
[1, 2, 3, 4]
🪄Anytime Magic Tip:
The mutating keyword is enforced by the Swift compiler, which ensures that a method cannot modify the state of an instance without explicitly being marked as mutating. This helps prevent unexpected changes to the state of an instance, which can lead to bugs and errors.
What is the difference between @StateObject and @ObservedObject in SwiftUI?
@StateObject
and @ObservedObject
are two ways to manage the state of an object in SwiftUI.
@StateObject
is used when you want to create an instance of an object that will manage the state of a view. It creates and owns the object, and keeps it alive for the view's lifetime. You can also modify the object's state using this property wrapper.
On the other hand, @ObservedObject
is used when you already have an instance of an object that you want to use to manage the state of a view. It does not create or own the object, but only observes it. This means that the object's lifetime is not managed by this property wrapper.
However, there are some key differences between the two:
-
Ownership: The
@StateObject
creates and owns an instance of an object, while the @ObservedObject only observes an instance that is passed in or injected in. -
Initialization: The
@StateObject
requires an initializer for the object it creates, while the@ObservedObject
requires an instance of the object. -
Lifetime: The
@StateObject
keeps the object alive for the lifetime of the view, while the@ObservedObject
doesn't create or own the object. -
Mutability: The
@StateObject
allows the object to be mutated within the view, while the@ObservedObject
does not.
What are Mocks and Stubs and what is the difference between them?
Mocks:
A mock is a version of a test double that can be used to verify interactions between objects. Mocks are used to check, if a certain method was called with expected parameters. They often contain expectations about the interactions that will happen during the test.
Mocks are used when you need to verify that a particular interaction occurred, such as ensuring that a method was called a certain number of times with specific arguments.
Example:
class FollowersServiceMock: FollowersService {
var fetchFollowersCalled = false
var fetchFollowersUserId: String?
var completion: (([Follower]) -> Void)?
func fetchFollowers(userId: String, completion: @escaping ([Follower]) -> Void) {
fetchFollowersCalled = true
fetchFollowersUserId = userId
self.completion = completion
}
// Helper method to simulate completion
func completeWithFollowers(_ followers: [Follower]) {
completion?(followers)
}
}
// Test using the mock
func testFetchFollowersWithMock() {
let followersServiceMock = FollowersServiceMock()
var receivedFollowers: [Follower] = []
followersServiceMock.fetchFollowers(userId: "123") { followers in
receivedFollowers = followers
}
// Verify that fetchFollowers was called
XCTAssertTrue(followersServiceMock.fetchFollowersCalled)
XCTAssertEqual(followersServiceMock.fetchFollowersUserId, "123")
// Simulate completion with mock data
let mockFollowers = [
Follower(id: "1", name: "Alice"),
Follower(id: "2", name: "Bob")
]
followersServiceMock.completeWithFollowers(mockFollowers)
// Verify received followers
XCTAssertEqual(receivedFollowers.count, 2)
XCTAssertEqual(receivedFollowers[0].name, "Alice")
XCTAssertEqual(receivedFollowers[1].name, "Bob")
}
Stubs:
A stub is a simplified implementation of an object that returns predefined responses. It is primarily used to provide data that a test needs to run. Stubs are usually static and do not have behaviour beyond what is required for the test. They do not record interactions.
Stubs are used when you need to control the indirect inputs of the system under test. For example, if a method depends on an external service or a database, you can use a stub to return fixed data without calling the actual service or database.
Example:
protocol FollowersService {
func fetchFollowers(userId: String, completion: @escaping ([Follower]) -> Void)
}
struct Follower {
let id: String
let name: String
}
class FollowersServiceStub: FollowersService {
func fetchFollowers(userId: String, completion: @escaping ([Follower]) -> Void) {
// Return a predefined list of followers
let followers = [
Follower(id: "1", name: "Alice"),
Follower(id: "2", name: "Bob")
]
completion(followers)
}
}
// Test using the stub
func testFetchFollowersWithStub() {
let followersServiceStub = FollowersServiceStub()
var receivedFollowers: [Follower] = []
followersServiceStub.fetchFollowers(userId: "123") { followers in
receivedFollowers = followers
}
XCTAssertEqual(receivedFollowers.count, 2)
XCTAssertEqual(receivedFollowers[0].name, "Alice")
XCTAssertEqual(receivedFollowers[1].name, "Bob")
}
Key Differences
Purpose:
- Stubs provide predefined responses. They are used to supply indirect inputs to the system under test.
- Mocks verify the interactions. They are used to check if certain methods are called with expected parameters.
Behavior:
- Stubs are typically static and return fixed data.
- Mocks are dynamic and can record and verify method calls and parameters.
Usage:
- Stubs are used when the focus is on the output of the system under test.
- Mocks are used when the focus is on the interaction between objects.
- By understanding these differences, you can choose the appropriate type of test double for your testing scenario in Swift.
Explain Equatable, Hashable, and Comparable protocol in Swift.
Equatable Protocol
This protocol allows two objects to be compared for equality using the == operator. Equatable is also the base protocol for the Hashable and Comparable protocols, which allow more uses of your custom type, such as constructing sets or sorting the elements of a collection.
Here is an example of the Equatable protocol:
struct User: Equatable {
let firstName: String
let lastName: String
static func == (lhs: User, rhs: User) -> Bool {
return lhs.firstName rhs.firstName && lhs.lastName rhs.lastName
}
}
let alex1 = User(firstName: "Alex", lastName: "Bush")
let alex2 = User(firstName: "Alex", lastName: "Daway")
let alex3 = User(firstName: "Alex", lastName: "Bush")
print(alex1 == alex2) // print: false
print(alex1 == alex3) // print: true
Hashable Protocol:
This protocol enables instances of a class to be used as keys in a dictionary. To conform to Hashable, an instance must implement the hashValue property, which returns an Int. When two instances are equal, they must have the same hash value.
struct User: Hashable {
let firstName: String
let lastName: String
}
let alex1 = User(firstName: "Alex", lastName: "Bush")
let alex2 = User(firstName: "Alex", lastName: "Daway")
let alex3 = User(firstName: "Alex", lastName: "Bush")
print("Alex1 hash value: \(alex1.hashValue)")
print("Alex2 hash value: \(alex2.hashValue)")
print("Alex3 hash value: \(alex3.hashValue)")
// Output:
// Alex1 hash value: 8130103028233918979
// Alex2 hash value: 1859436915308576384
// Alex3 hash value: 8130103028233918979
In the above example, you can see that the objects alex1 and alex3 have the same hash values because they both have identical properties.
Note: To compare the selective properties of the type, you may use the hash(into hasher: inout Hasher) function inside the type.
Comparable Protocol:
The Comparable protocol in Swift enables instances of a class to be compared for ordering. To conform to Comparable, an instance must implement the < operator, which returns a Bool indicating whether the left-hand side instance is less than the right-hand side instance.
struct Point: Comparable {
let x: Int
let y: Int
static func < (lhs: Point, rhs: Point) -> Bool {
return lhs.x < rhs.x && lhs.y < rhs.y
}
}
let pointA = Point(x: 0, y: 0)
let pointB = Point(x: 1, y: 4)
let pointC = Point(x: 2, y: 2)
print(pointA < pointB) // true
print(pointB < pointC) // false
print(pointA < pointC) // true
🪄Anytime Magic Tip:
-
In Swift, many types conform to the Equatable protocol by default like Int, Double, String, Array etc.
-
In the Hashable protocol, hash values cannot be guaranteed to be equal across different executions of your program. Do not save hash values for use during future executions.
-
When conforming to multiple protocols, make sure that they are consistent with each other. For example, if two instances are equal, they should have the same hash value and have the same ordering when compared.
What does UIApplicationMain mean?
The @UIApplicationMain
attribute is used in Swift to designate the entry point for an iOS, iPadOS, or macOS app that uses a graphical user interface (GUI).
When you add the @UIApplicationMain
attribute to a class that conforms to the UIApplicationDelegate
protocol, it tells the compiler that this class contains the main entry point for the application. This class must also include a main()
function that serves as the starting point for the app.
The UIApplicationDelegate
protocol defines methods that manage the application's life cycle, such as launching, backgrounding, and terminating the app. By conforming to this protocol, your app's main class can implement these methods to respond appropriately to changes in the app's state.
The class to which you add @UIApplicationMain
attribute must conform to the UIApplicationDelegate
protocol. This defines a set of methods that manage the app's life cycle. These methods include application(_:didFinishLaunchingWithOptions:)
, applicationDidEnterBackground(_:)
, applicationWillEnterForeground(_:)
, and others.
Your app's main class can implement these methods to customize the behaviour of the app when it's launched, enters the background, or returns to the foreground.
If you need to access the UIApplication object from your app's main class, you can do so using the UIApplication.shared singleton instance.
Explain how Swift is a type-safe language?
There are several reasons behind how Swift is a type-safe language.
Strong Type:
In Swift, variables must be declared with a specific type, such as Int, Double, String, etc. Once declared, a variable cannot be assigned a value of a different type. For example,
var name: String = "John"
name = 12 // error: cannot assign value of type 'Int' to type 'String'
In the above example, you have declared a variable name
of string type. It will not be possible to assign a value of a different type later on.
Type Inference:
Swift also provides type inference, which means that you don't always have to explicitly specify the type of a variable. The compiler can automatically determine the type based on the value assigned to it. For example,
var name = "John"
name = 12 // error: cannot assign value of type 'Int' to type 'String'
In the above example, you have assigned a string value to the variable name
. It will not be possible to assign a value of a different type later on.
Type Casting:
In some cases, you may need to convert a value from one type to another. However, if the conversion fails, the program will crash at runtime. You can take precautions to prevent crashing the app. For example:
let number = "1990"
// We can use optional binding here to prevent crashing the app.
if let intValue = Int(number) {
print(intValue) // print: 1990
}
Optional types:
In Swift, variables can be declared as optional, which means they can either contain a value or be nil. This helps prevent errors caused by the use of uninitialized variables. For example:
var number: Int?
// We can use optional binding here to prevent crashing the app.
if let intValue = number {
print(intValue)
} else {
print("Value does not found") // print: Value does not found
}
In the above example, you can check for a valid value before using it instead of using it directly in the code.
Generics:
Swift also has support for generics, which allow you to write flexible functions and types that work with any type, while still maintaining type safety. For example:
func swapping<T>(_ Answer: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var number1 = 16
var number2 = 32
swapping(&number1, &number2)
print(number1, number2) // Output: 32 16
Why do you need escaping closures and when should you use them?
Escaping closures are required when we want to perform asynchronous tasks. Tasks like Networking calls where the execution takes more time in comparison to UI updates and we want to execute the next line of code without waiting for the network calls response. Once the response is available, we can jump back to the execution of code written in the closure block.
Closures are non-escaping by default and Xcode will complain if you use the closure at a place where it should have been explicitly an escaping one, for example:
You'll find in this example that we are trying to execute an asynchronous task with some delay. Once the function starts to execute, the parameters of the function are allocated a space in the memory and they retain in the memory until the function execution ends.
If we call this function in onAppear()
, then the compiler will execute the code till the last line of the function. The scope of execution will never come back to print the asynchronous task result because the execution scope is now out of function. The function's parameter will be dropped from the memory as soon as the function gets returned.
To fix this problem, we use escaping closures. Here, the function will not drop the escaping closure parameter from the memory until it reads the output of the closure, which basically mean that even if the function gets returned, the scope will come back to the closure execution line.
This is how the new version of the example will look like:
Output:
Where to go next?
Congratulations, you are one step ahead in your iOS interview preparation journey. If you are someone looking for an ultimate preparation resource for your next iOS interview then don't worry because we got you covered. We have made a highly comprehensive guide "Cracking the iOS Interview" Top 100+ iOS Interview questions and answers. More than 50 Senior iOS Experts working with top tech companies across the globe, have contributed to this e-Book.
Checkout this link to learn more!