Skip to main content

38 questions tagged with "General"

General tag description

View All Tags

What do the Comparable, Equatable, and Hashable protocols do?

4 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Equatable Implementing this protocol allows one instance of an object to be compared for equality with another instance of an object of the same type. You鈥檝e probably leveraged this protocol without realizing it as theEquatableprotocol is what allowsus to use == to check the equality of two objects.

Adding Equatable support to your object allows your object to gain access to many convenient Swift APIs automatically. Furthermore, since Equatable is the base protocol for Hashable and Comparable, by adding Equatable conformance,we can easily extend our implementation to support creating Sets, sorting elements of a collection, and much more.

Let鈥檚 take a look at our Money struct:

struct Money {
let value: Int
let currencyCode: String
}

Currently, there鈥檚 no easy way for us to check if one Money object is equal to another Money object.

We can change that by conforming to Equatable:

struct Money: Equatable {
let value: Int
let currencyCode: String

static func == (lhs: Money, rhs: Money) -> Bool {
lhs.currencyCode == rhs.currencyCode && lhs.value == rhs.value
}
}

Now, our adherence to the Equatable protocol and our subsequent custom implementation of the == operator allows us to easily compare any two Money objects for equality.

Technically, we did some extra work though...

Since Int and String types are Equatable themselves, Swift can automatically synthesize the == implementation for us.

So, it鈥檇 be sufficient to just write:

struct Money: Equatable {
let value: Int
let currencyCode: String
}

Hashable When an object implements the Hashable protocol it introduces a hashValue property which is useful in determining the equality of two objects and allows that object to be used with a Set or Dictionary.

The hashValue is an Integer representation of the object that will always be the same for any two instances that compare equally. In simple terms, this means that if we have two instances of an object A and B then if A == B it must follow that A.hashValue == B.hashValue.

However, the reverse isn鈥檛 necessarily true. Two instances can have the same hashValue, but may not necessarily equal one another. This is because the process that creates the hashValue can occasionally create situations where two different instances generate the same hashValue.

The Hashable protocol only guarantees that two instancesthat are already known to be equal will also have the same hashValue.

It鈥檚 easy to add support for the Hashable protocol, but note that Hashable requires conformance to the Equatable protocol as well.

From Swift 4.1 onwards, we can have the compiler automatically synthesize conformance for us just like we did with Equatable. This functionality is only an option when the properties within the object conform to Hashable.

struct Money: Hashable {
let value: Int
let currencyCode: String
}

Otherwise, we鈥檒l have to implement the hashValue generation ourselves. Swift 4.2 introduced a Hasher type that provides a randomly seeded universal hash function for us to use in our custom implementations:

struct Money: Hashable {
let value: Int
let currencyCode: String

func hash(into hasher:inout Hasher) {
hasher.combine(value)
hasher.combine(currencyCode)
}
}

Conforming to the Hashable protocol allows you to use this object in a Set or as a Dictionary key. Many types in the standard library conform to Hashable: Strings, Integers, Floats, Booleans, and even Set are hashable by default.

Comparable The Comparable protocol allows us to use our customtype with the <, <=, >=, and > operators.

The implementation of this protocol is quite clever. We only have to implement the less than operator < since the implementations of all ofthe other comparison operators can be inferred from < and our Equatable conformance.

This conformance allows us to use handy Swift methods like sorted(),min(), and max()on our objects in collections.

struct Money: Comparable {
let value: Int
let currencyCode: String

static func < (lhs: Money, rhs: Money) -> Bool {
lhs.value < rhs.value
}
}

What does app thinning mean?

2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

As the name suggests, app thinning - introduced in iOS 9 - helps developers reduce the size of their app鈥檚 executable. This reduction in size is accomplished through three mechanisms; slicing, bitcode, and the use of on-demand resources.

Prior to iOS 9, when you installed your app on a device, it would include assets for all device types. For example, you might be on a non-retina screen device, but the app installation would still include retina quality assets. In other words, your application鈥檚 executable would include all of the resources required to run on all device types.

Unsurprisingly, this would inflate the size of the installed app with assets the user would never be able to use. So, slicing allows developers to include only the relevant assets for the device on which the app is being installed. This is one of the many conveniences asset catalogs afford us.

Typically, when you upload your app to App Store Connect, you are sending a compiled binary file. When you enable the bitcode setting in Xcode, you鈥檒l send an intermediate representation of the compiled program instead. The compilation will be completed when it鈥檚 installed by the user. This delayed approach to compilation allows Apple to identify the device the app is being installed on and pick only the relevant resources and assets to bundle into the final executable alongside introducing additional device-specific optimizations.

On-demand resources are app contents that are hosted on the App Store and are separate from the related app bundle that you download. They enable smaller app bundles, faster downloads, and richer app content. The app requests sets of on-demand resources, and the operating system manages downloading and storage. The app uses the resources and then releases the request. After downloading, the resources may stay on the device through multiple launch cycles, making access even faster.

Nowadays, these settings are enabled by default.

What does the CodingKey protocol allow you to do?

2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

In the event that the keys in a JSON response do not match the variable names in your Codable model exactly, you can use the CodingKey protocol to bridge specific properties that differ only in naming.

Simply put, the CodingKey protocol provides you with more granular control of the Codable protocol鈥檚 serialization and deserialization behavior.We can utilize thisprotocolwith the help of a nested enum defined within our Codable model.

Here鈥檚 an example JSON:

{
"url": "https://dynaimage.cdn.cnn.com/cnn/best-cakes.jpg",
"img_caption": "Japanese Dessert",
"img_source": "CNN"
}

We may want to pick more Swifty names for our variables, so our corresponding Codable model may look like this:

struct Article: Codable {
let url: String
let caption: String
let source: String
}

Since the property names and the JSON response鈥檚 keys differ,Codable鈥檚 default deserialization behavior will fail here.

We鈥檒l need to implement the CodingKey protocol:

struct Article: Codable {
let url: String
let caption: String
let source: String

enum CodingKeys: String, CodingKey {
case url
case caption = "img_caption"
case source = "img_source"
}
}

Now, we are able to influence Codable鈥檚 deserialization behavior and the JSON response can be converted to an Article without issue.

What does the File鈥檚 Owner do?

2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

When you load a .xib file and specify the owner property,the class responsible for loading the .xib now becomes the File's Owner.

open func loadNibNamed(_ name: String, owner: Any?,
options: [UINib.OptionsKey : Any]? = nil) -> [Any]?

// In ViewController.swift
Bundle.main.loadNibNamed("ExampleView", owner: self, options: nil)

Put simply, the File鈥檚 Owner is responsible for loading the .xib and facilitating communication between the code and the elements defined in the view. In the example above, ViewController initiates loading the .xib file therebymaking it the File鈥檚 Owner. As a result, it will now serve as the middle-man between the .xib file and our application鈥檚 code.

Once the .xib file has completed loading, the File鈥檚Owner is responsible for managing the view's contents and binding all of the declared IBOutlets and IBActions in your code to the view's corresponding UI components.

When we specify the File鈥檚 Owner in our .xib directly, we're effectively assigning a placeholder value that says -"This class will load me, interact with my UI, and create the necessary bindings to my various UI elements".

It's important to recognize that the File鈥檚 Owner is an independent entity and not a part of, nor bound to, the .xib itself - it鈥檚 just the class that assumes this middle-man responsibility.

What features of Swift do you like or dislike?

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

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Unfortunately, there鈥檚 no one-size-fits-all answer for a question like this. These types of open-ended questions help the interviewer gauge your understanding and level of depth with a language or technology.

Is the candidate able to compare and contrast it with other languages that solve some implementation problems differently? Does the candidate follow Swift's development? Can they defend their technical opinions?

A common answer to this question is that Swift's optional chaining functionality violates the Law of Demeter.

What is a .dSYM file?

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

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Whenever we upload our app to Apple, we remove some information (called symbols) from the compiled executable which specify exactly what functions and variables are being referenced.

This intentional process of removing this data from our executable can not only help reduce the size of our application's binary, but also helps in making our application more difficult to reverse engineer.

Without the inclusion of these symbols, our crash logs look like this:

0 libswiftCore.dylib 0x000000018f3c93800x18f394000 + 217984
1 libswiftCore.dylib 0x000000018f3c93800x18f394000 + 217984
2 libswiftCore.dylib 0x000000018f3c88440x18f394000 + 215108
3 libswiftCore.dylib 0x000000018f3a74e00x18f394000 + 79072
4 libswiftCore.dylib 0x000000018f3ab0d80x18f394000 + 94424
5 F49088168M 0x00000001045ac7500x104590000 + 116560
6 F49088168M 0x00000001045b79040x104590000 + 162052
7 F49088168M 0x00000001045b897c0x104590000 + 166268
8 F49088168M 0x000000010459d9140x104590000 + 55572
9 F49088168M 0x00000001045a0e700x104590000 + 69232
10 F49088168M 0x00000001045a0f4c0x104590000 + 69452

Clearly, it鈥檚 hard to tell what鈥檚 going on - all we see are memory addresses.

That鈥檚 where the .dSYM file comes in.

The .dSYM file (debug symbol file) contains the informationrequired to convert a stack-trace into a human-readable format. This file is automatically created with every release build and is used by Xcode to put the symbols back into the crash report thereby allowing you to read it properly.

Through a process known as re-symbolication, we can leverage our .dSYM file to convert our crash logs to something like this instead:

0 libswiftCore.dylib 0x000000018f3c9380 closure # 1 in closure # 1 in closure # 1 in _assertionFailure+ 217984 (_:_:file:line:flags:) + 452
1 libswiftCore.dylib 0x000000018f3c9380 closure # 1 in closure # 1 in closure # 1 in _assertionFailure+ 217984 (_:_:file:line:flags:) + 452
2 libswiftCore.dylib 0x000000018f3c8844 _assertionFailure+ 215108 (_:_:file:line:flags:) + 468
3 libswiftCore.dylib 0x000000018f3a74e0 _ArrayBuffer._checkInoutAndNativeTypeCheckedBounds+ 79072 (_:wasNativeTypeChecked:) + 208
4 libswiftCore.dylib 0x000000018f3ab0d8 Array.subscript.getter + 84
5 F49088168M 0x00000001045ac750 static ELM327ResponseManager.getResponse(responseStr:obd2Protocol:) + 116560 (ELM327ResponseManager.swift: 27 )
6 F49088168M 0x00000001045b7904 ELM327Client.dataInput(_:characteristicUuidStr:) + 162052 (ELM327Client.swift: 56 )
7 F49088168M 0x00000001045b897c protocol witness for BLEClientInputPort.dataInput(_:characteristicUuidStr:) in conformance ELM327Client + 166268 (<compiler-generated>:0)
8 F49088168M 0 x000000010459d914 BLEConnection.peripheralDataReceived(data:characteristicUuidStr:) + 55572 (BLEConnection.swift:124)
9 F49088168M 0 x00000001045a0e70 BLEConnection.peripheral(_:didUpdateValueFor:error:) + 69232 (BLEConnection.swift:293)
10 F49088168M 0 x00000001045a0f4c @objc BLEConnection.peripheral(_:didUpdateValueFor:error:) + 69452 (<compiler-generated>:0)

You鈥檒l see that our crash logs now contain real method and variable names which makes debugging far easier.

Some services, like Crashlytics, will automatically re-symbolicate the crash reports for you so they're more human readable. This process allows us to ensure our crash logs are obfuscated for everyone else, but still readable and useful to us as developers.

Simply put, removing these symbols from our executable helps us ensure that our app is not only difficult to reverse engineer, but also allows us to reduce our application鈥檚 binary size. Then, when needed, we can use the .dSYM file to reversethe process.

What is code coverage?

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

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Code coverage allows you to measure what percentage of your codebase is being exercised by your unit tests. More importantly, it helps you identify what sections of your codebase your tests don鈥檛 cover.

This option is disabled by default.

To enable it for your project, edit the target鈥檚 settings and select the Code Coverage checkbox:

Now, when you run your tests, you鈥檒l see a breakdown of what areas of your codebase are covered by tests.

What is operator overloading?

2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Operator overloading allows you to change how existing operators interact with custom types in your codebase. Leveraging this language feature correctly can greatly improve the readability of your code.

For example, let鈥檚 say we had a struct to representmoney:

struct Money {
let value: Int
let currencyCode: String
}

Now, imagine we鈥檙e building an e-commerce application. We鈥檇 likely need a convenient way of adding up the prices of all items in our shopping cart.

With operator overloading, instead of only being able to add numeric values together, we could extend the+operator to support adding Money objects together. As part of this implementation, we could even add logic to convert one currency type into another!

When we want to create our own operator, we鈥檒l need to specify whether it's of theprefix, postfix, orinfixvariety.

prefix: describes an operator that comes before thevalue it is meant to be used with (e.x. !isEmpty)

postfix: describes an operator that comes after thevalue it is meant to be used with (e.x. the force-unwrapping operator -user.firstName!)

infix: describes an operator that comes in betweenthe value it is meant to be used with and is the most common type (e.x. +, -,* are all infix operators)

In order to take advantage of this language feature, we just need to provide a custom implementation for the operator in our type's implementation:

struct Money {
let value: Int
let currencyCode: String

static func + (left: Money, right: Money) -> Money {
return Money(value: left.value + right.value, currencyCode: left.currencyCode)
}
}

let shoppingCartItems = [
Money(value: 20 , currencyCode: "USD"),
Money(value: 10 , currencyCode: "USD"),
Money(value: 30 , currencyCode: "USD"),
Money(value: 50 , currencyCode: "USD"),
]

// Output: Money(value: 110, currencyCode: "USD")
print(shoppingCartItems.reduce(Money(value: 0 , currencyCode: "USD"), +))

What is the difference between a dynamic library and a static library?

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

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

As your application matures and your application size and launch speed start to suffer, you'll likely find yourself re-evaluating how you integrate libraries into your project.

Libraries and frameworks can either be linked statically or dynamically.

Static libraries are collections of object files (the machine code output after compilation) grouped into a single containing resource. This library will then be copied into the larger executable that eventually runs on your device. If you鈥檝e ever seen a file ending in.a, it鈥檚 a static library.

Imagine a suitcase filled with everything you need for your vacation. A static library is similar; everything you need in order to run your application is included in the executable itself. Static libraries cannot contain images, sound files, media,etc. - they can only store code files.

Dynamic libraries (.dylibfiles) are loaded into memorywhen needed instead of being bundled with the executable itself. All iOS and macOS system libraries are actually dynamic.

The main advantage here is that any application that relies on these dynamic libraries will benefit from all future speed improvements and bug fixes in these libraries without having to create a new release. Additionally, dynamic libraries are shared between applications, so the system only needs to maintain one copy of the resource. Since it鈥檚 shared and only loaded when needed, invoking code in a dynamic library is slower than a static one.

Let鈥檚 take a detailed look at the advantages and disadvantages:

Static Libraries

Pros

  • Guaranteed to be present and the correct version at runtime.
  • The application can run without any external dependencies. You don鈥檛 need to monitor library updates since the object files are part of the executable itself. As a result, it becomes standalone and can move from platform to platform.
  • Faster performance compared to calls made to a dynamic library.

Cons

  • Makes the executable larger as it simply just contains more code.
  • Your application will be slower to launch as the library needs to be loaded into memory.
  • Any changes in a static library require the application to be re-compiled and re-distributed.
  • You have to integrate the entire library even if you only rely on a small portion of it.

Dynamic Libraries

Pros

  • Doesn鈥檛 increase app size.
  • Better application launch speech as the library is loaded only when needed.
  • Only the relevant section of the library needed for the currently executing code is loaded instead of loading the library in its entirety.

Cons

  • Application may crash if the library updates are not compatible with your application (i.e. business logic / iOS version).
  • Application may crash if the dynamic library cannot be loaded or found.
  • Calls to dynamic library functions are slower than calls made to a static library.

There鈥檚 no one size fits all answer. You鈥檒l have to make a pragmatic decision and weigh how much you value performant library calls, app size, launch time, etc. and pick the approach or a hybrid approach that best suits your needs.

What is the difference between static and dynamic dispatch?

2 min read
Ace the iOS Interview
Aryaman Sharda
Sources & Resources

Main Source: 馃敆 Ace the iOS Interview

Additional Sources:

Further Reading:

TL/DR

Swift, like many other languages, allows a class to override methods and properties declared in its superclass. As a result, the program has to determine at runtime the correct version of the method or property to use.

This process is called dynamic dispatch.

Dynamic dispatch is implemented with the help of a method table (a.k.a. witness table). The witness table is used to figure out which implementation of a function to call - the superclass鈥檚 implementation or the subclass鈥檚 implementation.

Dynamic dispatch enables polymorphism which allows us to increase the expressiveness of our code, but it introduces a constant amount of runtime overhead every time we call a function. As a result, you鈥檒l typically want to avoid polymorphism in performance sensitive code.

If you don鈥檛 need dynamic dispatch (you don鈥檛 want the function or property to be overridden), you can improve performance by using the final keyword which prohibits overriding. This will allow us to use static dispatch instead which doesn鈥檛 incur this performance penalty. Moreover, static dispatch allows us to leverage additional compiler optimizations.

Finally, static dispatch is supported by both value and reference types. Dynamic dispatch is only supported by reference types as it requires inheritance and value types can鈥檛 support inheritance.