iOS Development Q & A
Below is a summary of notes taken during my Swift/iOS development study.
TOC
Data Structure
Q: What are the differences between struct
and class
?
A: struct
and class
have the following features in common:
- both can define properties to store values, and they can define functions
- both can define subscripts to provide access to values with subscript syntax
- both can define initializers to set up their initial state, with
init()
- both can be extended with extension
- both can conform to protocols
- both can work with generics to provide flexible and reusable types
They also have the following differences:
struct:
- value type
- doesn’t support inheritance
- stored on stack, faster compared to heap
- immutable when declared as
let
- thread safe
class:
- reference type
- supports inheritance unless defined as
final
- stored on heap, slower compared to stack
- can be deinitialized using
deinit()
- a
let
class instance can still be mutable - can be identity checked by
===
- same instance can be referenced more than once
Stack and Heap:
- Stack is used for static memory allocation and Heap for dynamic memory allocation, both stored in RAM. Allocation of stack is dealt with in compile-time, thus optimisation is possible.
- An important note to keep in mind is that in cases where a value stored on a stack is captured in a closure, that value will be copied to the heap so that it’s still available by the time the closure is executed.
Q: What are the common use cases of struct
and class
?
A:
struct:
- simple data types, e.g.
CGRect
, data models that are well defined without complex relationship between objects - thread safety is concerned in a multi-thread context
- no need for inheritance
- immutability ie.
let
struct instance
class:
- inheritance is needed, e.g. a base class that’s designed to be inherited
- deinitialization is needed, e.g. cleanup a flow
- the same object that need to be referenced by multiple parties
- copying doesn’t make sense, e.g.
UIViewController
- Objective-C compatibility via
@obj
Q: What do you need to do, to have a mutable struct
?
A: Mark methods that change the internal state as mutating; Also, the variable of this struct type need to be var
instead of let
.
Q: For Swift enum
, how does raw value differ from associated value?
A:
Raw values are constants associated to enum
cases, Swift doesn’t allow duplicate values because each enum
case must have a unique value. An enum
can be converted to its raw value by using the rawValue
property, and there’s dedicated initializer to convert a raw value to an enum
instance.
Associated values are data associated to enum
case, cases can have zero or more such values.
Q: Can you name two benefits of using subclassing instead of enums with associated types?
A: A superclass prevents duplication; no need to declare the same property twice. With subclassing, you can also override existing functionality.
Q: Can you name two benefits of using enums with associated types instead of subclassing?
A: No need to refactor anything if you add another type, whereas with subclassing you risk refactoring a superclass and its existing subclasses. Second, you’re not forced to use classes.
Q: Which raw types are supported by enums?
A: String, character, and integer and floating-point types.
Q: Are an enum’s raw values set at compile time or runtime?
A: Raw type values are determined at compile time.
Q: Are an enum’s associated values set a compile time or runtime?
A: Associated values are set at runtime.
Q: Which types can go inside an associated value for enum?
A: All types fit inside an associated value.
Q: How can enums references themselves as associated value?
A: You can use indirect enum, they are called “indirect” because they modify the way Swift stores them so they can grow to any size. Without the indirection, any enum that referenced itself could potentially become infinitely sized: it could contain itself again and again, which wouldn’t be possible.
Q: How could you preserve a struct’s memberwise initializer while having your custom initializer?
A: Define your custom initializer in an extension.
Q: How could you list all cases of an enum?
A: Swift has a CaseIterable
protocol that automatically generates an array property of all cases in an enum. To enable it, all you need to do is make your enum conform to the CaseIterable
protocol and at compile time Swift will automatically generate an allCases property that is an array of all your enum’s cases, in the order you defined them.
Q: What is a tuple?
A: A tuple is a data structure like an anonymous struct, it can hold different types of data and it can be created on the fly. It’s commonly used to return multiple values from a function call. Tuples can be accessed using element names if provided, or using a position in the tuple, e.g. 0 and 1. While element names are not mandatory, it’s a good idea to have them, so the code is more readable. Why return a tuple instead of specific struct
? When the return values are simple enough and not reused in different places, especially in cases like returning two elements from an array which doesn’t make sense to live in a specific struct.
Memory Management
Q: What is ARC?
A: Automatic Reference Counting is a memory management method Swift uses to determine when objects should be deallocated. Each object has a reference count, and when the reference count reaches 0 the object is deallocated.
An object’s reference count is increased by 1 when a strong reference is assigned to that object. An object’s reference count is decreased by 1 when a strong reference is removed from that object.
Q: What is retain cycle?
A: It’s possible to write code in which an instance of a class never gets to a point where it has zero strong references. This can happen if two class instances hold a strong reference to each other, such that each instance keeps the other alive. This is known as a strong reference cycle.
Q: What are the difference between weak
and unowned
references?
A:
A weak
reference does not increment or decrement the reference count of an object. Since weak
references do not increment the reference count of an object, a weak
reference can be nil
, meaning the object could be deallocated while the weak reference is still pointing to it.
Like a weak
reference, an unowned
reference does not increment or decrement the reference count of an object. However, unlike a weak
reference, the program guarantees to the Swift compiler that an unowned
reference will not be nil when it is accessed.
Q: Do UIView Animation Blocks Require Weak or Unowned Self?
A: Not all blocks require weak or unowned self to prevent retain cycles. UIView animation blocks run only once and are then deallocated. This means even strong references in animation blocks will be released when the block runs.
Q: Are weak and unowned only used with self inside closures?
A: No, you can indicate any property or variable declaration weak or unowned as long as it’s a reference type.
Q: What will the code print?
var thing = "day"
let closure = { [thing] in
print("To\(thing)")
}
thing = "morrow"
closure()
A:
It’ll print: “Today”.
The capture list creates a copy of thing when you declare the closure. This means that captured value doesn’t change even if you assign a new value to thing.
If you omit the capture list in the closure, then the compiler uses a reference instead of a copy. Therefore, when you invoke the closure, it reflects any change to the variable.
Q: Are closure value or reference types?
A: Closures are reference types. Assigning a closure to a variable, copy the variable into another, it’s still the reference being copie with its capture list.
Q: How would you identify and resolve a retain cycle?
A: There are tools in Instruments that shows memory leaks, which can be a start point for debugging retain cycle; Also, Memory Graph Debugger can be used to visualize memory allocations, which helps identifying retain cycles; Once a retain cycle is identified, we need to decide what should be made weak in order to break the cycle.
Optionals
Q: What is an optional:
A: Optional is a Swift feature that a variable of any type represent either a value or a lack of value. In Objective-C, similar idea is only available for reference types using the nil
special value. Value types, such as int or float, do not have this ability.
Q: How do you assign an optional to a variable with a fallback value if the optional is nil
?
A: Use nil-coalescing operator ??
, it falls back to a default value, but also unwraps the optional if it does have a value.
Q: What is optional chaining?
A: When you need a value from an optional property that can also contain another optional property.
Q: When do we use optional chaining vs if let
or guard
?
A: Optional chaining is used when we do not really care if the operation fails.
Q: Can you describe how to convert an optional Boolean property, say, feature toggle, which can be enabled, disabled or not set, into an enum?
A:
enum FeatureToggle: RawRepresentable {
case enabled
case disabled
case notSet
init(rawValue: Bool?) {
switch rawValue {
case true?: self = .enabled
case false?: self = .disabled
default: self = .notSet
}
}
var rawValue: Bool? {
switch self {
case .enabled: return true
case .disabled: return false
case .notSet: return nil
}
}
}
Q: When is force unwrapping acceptable?
A: Ideally force unwrapping should not be used, it tries to convert an optional to a value regardless of it contains a value or not, so if it contains .none
, nil
, it triggers an error that crashes the app. But sometimes it cannot be avoided because otherwise the app can end up in a bad state, e.g. the app can’t recover from a nil
value:
- postponing error handling: when you want to produce working code quickly for success scenario
- when you know better than the compiler: e.g. creating an
URL
from a string
Q: Possible cases when implicitly unwrapped optionals cannot be avoided?
A:
- When you cannot initialize a property that is not nil by nature at instantiation time. A typical example is an Interface Builder outlet, which always initializes after its owner. In this specific case — assuming it’s properly configured in Interface Builder — you’ve guaranteed that the outlet is non-nil before you use it.
- To solve the strong reference cycle problem, which is when two instances refer to each other and require a non-nil reference to the other instance. In such a case, you mark one side of the reference as unowned, while the other uses an implicitly unwrapped optional.
Q: If no optionals in a function are allowed to have a value, what would be a good tactic to make sure that all the optionals are filled?
A: Use guard
to block optionals at the start of a function.
Q: If a number of functions are executed in different paths depending on the optionals inside them, what would be a correct approach to handle all these paths?
A: Put multiple optionals inside a tuple allows you to pattern match on them and take different paths in a function.
Q: What are good alternatives to implicit unwrapped optional?
A: Lazy properties or factories that are passed via an initializer.
Q: What are the various ways to unwrap an optional? How do they rate in terms of safety?
A: There are several ways:
var x : String? = "Test"
Forced unwrapping — unsafe.
let a: String = x!
Implicitly unwrapped variable declaration — unsafe in many cases.
var a = x!
Optional binding — safe.
if let a = x {
print("x was successfully unwrapped and is = \(a)")
}
Optional chaining — safe.
let a = x?.count
Nil coalescing operator — safe.
let a = x ?? ""
guard
statement — safe.
guard let a = x else {
return
}
Optional pattern — safe.
if case let a? = x {
print(a)
}
Q: What’s the difference between nil and .none?
A: There is no difference, as Optional.none (.none for short) and nil are equivalent. In fact, this statement outputs true:
nil == .none
The use of nil
is more common and is the recommended convention.
Q: What happens when you call map
on optionals?
A: Mapping on optionals lets you perform actions on the optional as if the optional were unwrapped, so inside map
’s closure you don’t need to know or deal with optional, this saves you some boilerplate code for unwrapping with temporary variables manually. On top of this, you can also chain several mapping operations.
Protocols and Generics
Q: What is Protocol Extension?
A: Protocol extension is to extend a protocol and provide default implementation of methods, so that classes that conform to the protocol will have the default implementation for free, and classes can selectively override protocol methods just like class inheritance. The benefit of this is that a class can have 0 or 1 superclass but protocols don’t have such constraint.
Q: Explain how a function with a generic type parameter works behind the scene.
A: Swift creates multiple functions behind the scenes for different types of the generic parameter types, this process is called monomorphization where the compiler turns polymorphic code into concrete singular code. Swift is clever enough to prevent all combinations of concrete parameter types that lead to large binaries. Swift uses several measures involving metadata to limit the amount of code generation. In the case of function with generic parameter type, the compiler creates a low-level function; For relevant types, Swift generates metadata, called value witness tables. At runtime, Swift passes the corresponding metadata to the low-level representation of generic parameter type when needed.
Q: What is invariance for Swift’s generics? Is there any exception?
A: Swift’s generics are invariant, meaning even if a generic type wraps a subclass, it does not make it a subtype of a generic wrapping its superclass. Invariance is a safe way to handle polymorphism. Swift’s builtin generic types, such as Array
or Optional
, do allow for subtyping with generics. e.g. where it expects Array<BaseClass>
, it’s OK to pass an instance of type Array<SubClass>
.
Q: Given the below protocol:
protocol SomeProtocol {}
What is the difference between the following statements?
func doWork(_ something: SomeProtocol) {}
func doWork<T: SomeProtocol>(_ something: T) {}
A: The nongeneric function uses dynamic dispatch (runtime), the generic function is resolved at compile time.
Q: What are the differences between using a protocol as a type, and using generics that conform to the protocol?
A: Using a protocol as a type speeds up programming and makes mixing and swapping things around easier. Generics are more restrictive and wordy, but they give you performance benefits and compile-time knowledge of the types you’re implementing.
Q: Is there any problem with the following code:
protocol Input {}
protocol Output {}
protocol Processor {
@discardableResult
func start(input: Input) -> Output
}
func batchProcess(processor: Processor, input: [Input]) {
input.forEach { value in
processor.start(input: value)
}
}
A: For every type you want to use for the input and output, they have to conform to Input
and Output
protocols, including String
, URL
, etc, which ends up with boilerplate code. Another downside is that you’re introducing a new protocol for each parameter and return type. Lastly, adding a new method on Input
or Output
would require the implementation on all the types that conform to the protocol.
One solution is to use associatedtype
: an associated type gives a placeholder name to a type that’s used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. One way to think of an associated type is that it’s a generic that lives inside a protocol. Conforming types can use the typealias keyword to specify the type that will replace each associated type placeholder. In many cases, however, Swift is able to infer that type from the context.
protocol Processor {
associatedtype Input
associatedtype Output
@discardableResult
func start(input: Input) -> Output
}
func batchProcess<P: Processor>(processor: P, input: [P.Input]) {
input.forEach { value in
processor.start(input: value)
}
}
Like generics, associated types get resolved at compile time.
Q: What is a Self requirement in a protocol?
A: A Self requirement is a placeholder in a protocol for a type, used as part of the protocol, which is replaced by the concrete type that conforms to the protocol. A Self requirement can thus be thought of as a special case of an associated type. Like a protocol with an associated type, a protocol with a Self requirement is also an incomplete protocol since the type to be used in place of the Self placeholder gets resolved only when the protocol is adopted. The difference is that, while a conforming type can use any type it wants to replace an associated type, any Self placeholder must be replaced by the confirming type itself.
Q: In the following code, how could you limit the Input
to be only of type String
?
func batchProcess<P: Processor>(processor: P, input: [P.Input]) {
input.forEach { value in
processor.start(input: value)
}
}
A:
func batchProcess<P>(processor: P, input: [P.Input]) where P: Processor, P.Input == String {
input.forEach { value in
processor.start(input: value)
}
}
Q: Given the following code, how could you apply protocol inheritance to further constrain it, say Input
has to be an URL and Output
has to be Data
?
protocol Processor {
associatedtype Input
associatedtype Output
@discardableResult
func start(input: Input) -> Output
}
func batchProcess<P>(processor: P, input: [P.Input]) where P: Processor, P.Input == URL, P.Output == Data {}
A:
protocol URLProcessor: Processor where Input == URL, Output == Data {}
func batchProcess<P: URLProcessor>(processor: P, input: [P.Input]) {}
Q: Given the following piece of code of a Broadcastor
with a default implementation, how could you add a default implementation for it that also sanitise the message before broadcasting, using protocol inheritance? And how could you do the same with protocol composition? What are the downsides of each approach?
protocol Broadcaster {
func send(_ message: String)
}
extension Broadcaster {
func send(_ message: String) {
print("Message is sent!")
}
}
A:
Protocol inheritance:
protocol SanitisingBroadcaster: Broadcaster {
func sanitise(_ message: String) throws
}
extension SanitisingBroadcaster {
func send(_ message: String) {
guard try? sanitise(message) else {
preconditionFailure("Message invalid!")
}
print("Sending: \(message)")
}
func sanitise(_ message: String) throws {
// check message
}
}
struct OnlineBroadcaster: SanitisingBroadcaster {
// …
}
let broadcaster = OnlineBroadcaster()
broadcaster.send("Hello, world!")
- it doesn’t separate functionality and semantics. e.g. anything that sanitises message has to be a regular
Broadcastor
Protocol composition:
protocol BroadcastSanitiser {
func sanitise(_ message: String) throws
}
extension BroadcastSanitiser {
func sanitise(_ message: String) throws {
// …
}
}
struct OnlineBroadcaster: Broadcaster, BroadcastSanitiser {}
extension BroadcastSanitiser where Self: Broadcaster {
func send(_ message) {
guard try? sanitise(message) else {
preconditionFailure("Invalid message!")
}
print("Sending: \(message)")
}
}
let broadcaster = OnlineBroadcaster()
broadcaster.send("Hello, world!")
- composition approach may make the code too decoupled that implementers may not know precisely which method implementation is used under the hood
Q: What are conditional conformance?
A: Conditional comformance is conforming to a protocol only if certain conditions are true. For example, making Array
conforming to PlayerList
only if the array’s elements conform to Player
.
protocol Player {
var historyScore: Int { get }
}
extension Array where Element: Player {
func totalScore() -> Int {
return reduce(0) { result, element in
result + element.historyScore
}
}
}
Q: What’s the problem with the following code and how could you improve it?
protocol AdsProtocol {
func showAds()
}
extension UIViewController: AdsProtocol {
func showAds() {…}
}
A:
- assuming all view controllers need to conform to this protocol is probably not safe
- if the code is part of a framework, it will pollute the user of the framework that every view controller now gets the extension whether they like it or not
- it could clash with existing extension if they share the same name
extension AdsProtocol where Self: UIViewController {
func showAds() {…}
}
Now a view controller can choose to conform to AdsProtocol
and gets the method implementation for free on an as-needed basis.
Collections
Q: Explain Sequence
in Swift
A: A type that provides sequential, iterated access to its elements. A sequence is a list of values that can be stepped through one at a time, the most common way to iterate over the elements of a sequence is to use for-in
loop. This capability gives you access to a large number of operations that can be performed on any sequence, such like contains(_:)
.
To add Sequence
conformance to your own custom type, add a makeIterator()
method and returns an iterator. Alternatively, if your type can act as its own iterator, implementing the requirements of IteratorProtocol
and declaring conformance to both Sequence
and IteratorProtocol
.
struct Countdown: Sequence, IteratorProtocol {
var count: Int
mutating func next() -> Int? {
guard count > 0 else {
return nil
}
deter { count -= 1 }
return count
}
}
Q: Explain zip(_:_:)
A:
zip
creates a sequence of pairs built out of two underlying sequences.
let words = ["one", "two", "three", "four"]
let numbers = 1...4
for (word, number) in zip(words, numbers) {
print("\(word): \(number)")
}
// prints "one: 1"
// prints "two: 2"
// prints "three: 3"
// prints "four: 4"
If the two sequences are different lengths, the resulting sequence is the same length as the shorter sequence:
let naturalNumbers = 1...Int.max
let zipped = Array(zip(words, naturalNumbers))
// zipped == [("one", 1), ("two", 2), ("three", 3), ("four", 4)]
Q: What is Hashable
?
A: A type that can be hashed to produce an integer hash value.
The Hashable
protocol inherits from Equatable
protocol, which must also be satisfied.
- for
struct
: all its stored properties must conform toHashable
- for
enum
: all its associated values must conform toHashable
, anenum
without associated values hasHashable
conformance even without declaration
If use properties that don’t all conform to Hashable
, or if you want to have your own comparison for equality, say a Student
object may have a studentID
which is enough to uniquely identify a particular student, then you need to implement your own hash(into:)
method, combining the properties that need to be taken into account.
Q: What is subscripts?
A: Subscripts are shortcuts for accessing the member elements of a collection, list or sequence, or a custom type that supports subscripts.
Q: What is the difference between reduce
and reduce into
?
A:
The both can be used to produce a single value from the elements of an entire sequence.
reduce
reduce
iterates over a collection, taking an initial value and a closure which applies a transformation to that value. The closure provides two parameters, the partial result obtained in each iteration, and the next element of the collection.
reduce
makes sense when you are not creating expensive copies for each iteration, such as reducing into an integer.
reduce into
reduce into
passes the partial result by reference instead of value, it is preferred over reduce(_:_:)
for efficiency when the result is a copy-on-write type, for example an Array or a Dictionary.
Q: What is the Sequence
protocol and how could you implement it?
A: A type that provides sequential, iterated access to its elements. To add Sequence conformance to your own custom type, add a makeIterator() method that returns an iterator. Alternatively, if your type can act as its own iterator, implementing the requirements of the IteratorProtocol protocol and declaring conformance to both Sequence and IteratorProtocol are sufficient.
Q: What is AnyIterator
?
A: AnyIterator
is a type erased iterator, it accepts a closure when initialized, which is called whenever next
is called on the iterator.
Q: [coding] Create a Bin
that represents classified garbage bin that stores waste by types with number of count. e.g.
var bin = Bin<String>()
bin.insert("thought")
bin.insert("thought")
bin.insert("thought")
bin.insert("value")
bin.remove("thought")
bin.count // 3
print(bin)
// Output:
// thought occurs 2 times
// value occurs 1 time
let anotherBin: Bin = [1, 2, 2, 5, 5, 5]
print(anotherBin)
// Output:
// 1 occurs 1 time
// 2 occurs 2 times
// 5 occurs 3 times
A:
struct Bin<Element: Hashable>: IteratorProtocol {
private var store = [Element: Int]()
mutating func insert(_ element: Element) {
store[element, default: 0] += 1
}
mutating func remove(_ element: Element) {
store[element]? -= 1
if store[element] == 0 {
store[element] = nil
}
}
var count: Int {
store.values.reduce(0, +)
}
}
extension Bin: CustomStringConvertible {
var description: String {
var summary = String()
for (key, value) in store {
let times = value == 1 ? "time" : "times"
summary.append("\(key) occurs \(value) \(times)\n")
}
return summary
}
}
To implement Sequence
you need an iterator:
struct BinIterator<Element: Hashable>: Sequence, IteratorProtocol {
var store = [Element: Int]()
mutating func next() -> Element? {
guard let (key, value) = store.first else {
return nil
}
if value > 1 {
store[key]? -= 1
} else {
store[key] = nil
}
return key
}
}
Alternatively, we can implement Sequence
by returning a new AnyIterator
:
extension Bin: Sequence {
func make Iterator() -> AnyIterator<Element> {
var exhaustiveStore = store // create a copy
return AnyIterator<Element> {
guard let (key, value) = exhaustiveStore.first else {
return nil
}
if value > 1 {
exhaustiveStore[key]? -= 1
} else {
exhaustiveStore[key] = nil
}
return key
}
}
}
Finally, we can make Bin
implement ExpressibleByArrayLiteral
:
extension Bin: ExpressibleByArrayLiteral {
typealias ArrayLiteralElement = Element
init(arrayLiteral elements: Element...) {
store = elements.reduce(into: [Element: Int]()) { (updatingStore, element) in
updatingStore[element, default: 0] += 1
}
}
}
Q: What is IteratorProtocol
?
A:
It has an associated type element and that element is the type of the thing that you’re going to be pending or the type of the thing that you’re going to be iterating over, and it has one function called next
, which returns the next element and mutates that Iterator:
protocol IteratorProtcol {
associatedtype Element
mutating func next() -> Element?
}
Q: What are the differences between Sequence
and Collection
?
A:
Sequence
:
Sequence is a list of elements. It has two important caveats:
- it can be either finite or infinite
- you can only ever iterate through it one time, sometimes you will be able to iterate it more than once, but your not guaranteed to be able to iterate it more than once
Collection
:
Collection
inherits from Sequence
. Every Collection will always be finite, and you can iterate that Collection as many times as you want.
protocol Collection {
associatedtype Index: Comparable
var startIndex: Index
var endIndex: Index
subscript(position: Index) -> Iterator.Element { get }
func index(after index: Index) -> Index
}
Q: [coding] Create a TodoList
which is a Collection
, with every Day
mapping to one or more Activity
. Given the definition of Day
and Activity
below:
struct Activity: Equatable {
let date: Date
let description: String
}
struct Day: Hashable {
let date: Date
init(date: Date) {
let unitFlags: Set<Calendar.Component> = [.day, .month, .year]
let components = Calendar.current.dateComponents(unitFlags, from: date)
guard let convertedDate = Calendar.current.date(from: components) else {
self.date = date
return
}
self.date = convertedDate
}
}
A:
struct TodoList {
typealias DataType = [Day: [Activity]]
private var todos = DataType()
init(activities: [Activities]) {
self.todos = Dictionary(grouping: activities) { activity in
Day(date: activity.date)
}
}
}
extension TodoList: Collection {
typealias KeysIndex = DataType.Index
typealias DataElement = DataType.Element
var startIndex: KeysIndex { return todos.keys.startIndex }
var endIndex: KeysIndex { return todos.keys.endIndex }
func index(after i: KeysIndex) -> KeysIndex {
return todos.index(after: i)
}
subscript(index: KeysIndex) -> DataElement {
return todos[index]
}
}
extension TodoList {
subscript(date: Date) -> [Activity] {
return todos[Day(date: date)] ?? []
}
subscript(day: Day) -> [Activity] {
return todos[day] ?? []
}
}
extension TodoList: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Activity...) {
self.init(activities: elements)
}
}
Q: What is the difference between Array
and Set
in term of performance?
A: Set
is faster than Array
in general because elements in a Set
need to conform to the Hashable
protocol which allows Set
to be optimized for performance, it takes same amount of time to lookup an element in a small collection vs a large collection.
UIKit
Q: What are the pros and cons of creating UI programatically vs using storyboard?
A:
Storyboard is straightforward with drag and drop, gives visual representation of the screen and properties of the UI elements. Constructing UI programatically gives you a sense of control that you see the code of what you change on top of default values. Multiple people working on the same storyboard can create merge conflicts.
Q: When multiple people working on different screens defined inside a single storyboard, what is the technique to avoid merge conflict?
A: Use storyboard reference.
Q: What is unwind segue?
A: An unwide segue (sometimes called exit segue) can be used to navigate back through push, modal or popover segues, you can go back multiple steps in navigation hierarchy.
Q: What is the difference between the frame and the bounds?
A: The bounds of an UIView is the rectangle, expressed as location and size relative to its own coordinate system; The frame of an UIView is the rectangle, expressed as location and size relative to the superview it is contained within.
For UIScrollView
, its frame
can be (0, 0)
while its bounds can be non-zero, if its contentOffset
isn’t 0.
Q: How could you setup live rendering in storyboard?
A: Using @IBInspectable
and @IBDesignable
lets you build interface with custom controls and have them rendered in real-time during designing in storyboard.
@IBInspectable
properties provide access to user-defined runtime attributes. Accessible from the identity inspector, it’s basically a mechanism for configuring any key-value coded property of an instance in a NIB, XIB, or storyboard.
Built-in view types can also be extended to have inspectable properties beyond the ones already in Interface Builder’s attribute inspector. e.g. the following code exposes the cornerRadius
property of a view’s layer
:
extension UIView {
@IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
}
Prefixing @IBDesignable
(or IB_DESIGNABLE
macro in Objective-C) allows Interface Builder to perform live updates on a particular view.
Q: How could you design UI layout so it adapts to different devices, ie. iPhone and iPad?
A: On top of auto-layout, you can use size classes to custom your user interface for given device class, based on its orientation and screen size. Size classes let you add extra layout configuration to your app so that your UI works well across different devices.
Q: What is Intrinsic Content Size?
A: Intrinsic Content Size is something that a view can provide, to indicate the size needed for the view to render properly; It gives information to the Auto Layout engine that a particular view has a predefined size that the engine can use to calculate and lay it out among other views.
Q: What is the layer
object of a view?
A: It’s a data object that represents visual content. Layer objects are used by views to render their content. Custom layer objects can also be added to the interface to implement complex animations and other types of sophisticated visual effects.
Q: What is the “file owner” in interface builder?
A: The file owner is the object that loads the nib, i.e. the object which receives the message loadNibNamed:
or initWithNibName:
; If you want to access any objects in the nib after loading it, you can set an outlet in the file owner. By default View Controllers act as file owners, it’s external to the nib and not part of it, it’s only available when the nib is loaded.
Q: What is Safe area?
A: The safe area of a view reflects the area not covered by navigation bars, tab bars, toolbars and other system component that overlaps a view controller’s view. Additional insets can be specified via additionalSafeAreaInsets
.
Q: Difference between accessibilityIdentifier
, accessibilityLabel
and accessibilityValue
A:
accessibilityIdentifier
: An identifier can be used to uniquely identify an element in the scripts you write using the UI Automation interfaces. Using an identifier allows you to avoid inappropriately setting or accessing an element’s accessibility label.accessibilityLabel
: a succinct label that identifies the accessibility element, in a localized string.accessibilityValue
: the value of the accessibility element, in a localized string. Used when an accessibility element has static label and dynamic value, for example, a text field for message may have “message” as its label and the actual text as its value.
Q: How could you dictate the order in which views are read when VoiceOver is turned on?
A: You can use the method in your container view called index of accessibility element, which returns the index of the specified accessibility element, so you can arrange the order of the subviews in term of VoiceOver.
Q: What’s the difference between xib and storyboard?
A: Both for creating views using interface builder, xib is for view or single view controller, storyboard support multiple view controllers with transition flow.
Q: What is Dynamic Type?
A: Dynamic Type is a feature on iOS that enables the app’s content to scale based on the user’s preferred content size. It helps users who need larger text for better readability. And it also accommodates those who can read smaller text, allowing for more information to appear on the screen.
Q: How could you add shadow to a view?
A: In UIKit, all view layers have options for shadow opacity, radius, offset, color and path. In SwiftUI, you can use the shadow()
modifier. Dynamic shadow can be expensive with transparency calculations, rasterising the layer makes the shadow part of the overall bitmap which is less expensive but the current opacity of the layer is not rasterized.
Q: What are your experience using CoreGraphics?
A: Draw shapes and convert UIView
into UIImage
for saving locally.
Q: What are the benefits of using child view controllers?
A: Dividing a large view controller into child view controllers, the same functionality remains but in several smaller parts which are easier to maintain; Also, there can be cases where a view is reused as subviews in several screens (or view controllers), and the view has a number of business logic in it, which deserves a dedicated view controller.
Q: What are the pros and cons of using viewWithTag()
?
A: The only possible pro is that it provides a way to find a subview, but it should not be used just because you can, as using magic number isn’t a good idea.
Q: When would you choose to use a collection view rather than a table view?
A: Collection views can display tiles in columns and rows, it can also handle custom layouts, whereas table view is for linear lists with headers and footers. In iOS 14, UICollectionView
got a new feature that lets you include lists in the collection view.
Q: What happens when Color or UIColor has values outside 0 to 1?
A: In RGB color space, values outside of 0 to 1 should be clamped, but with the addition of wide colors, e.g. extended color space, it’s valid to have color values that are outside 0 to 1.
Concurrency
Q: Why do we need to specify self
to refer to a stored property or a method in asynchronous code?
A: Since the code is dispatched to a background thread, we need to capture a reference to the correct object.
Q: What is DispatchGroup?
A: DispatchGroup
allows for aggregate synchronization of work, you can use them to submit multiple different work items and track when they all complete.
Q: What are the common ways to perform asynchronous operations?
A:
Closures/Call backs
It’s a block of code in which we perform the operation and that block of code can be executed asynchronously. Usually it calls a completion block or delegate callback when the operation finishes.
Pros: simple for basic async tasks. Cons: it can become difficult to manage if the block has dependency on other block of code in the form of closure, you end up with nested closures which is hard to maintain.
Combine
In combine there are three main roles: subscriber, publisher and subscription
Subscriber subscribes to Publisher who creates and hands over Subscription; Upon receiving Subscription, Subscriber can request data so the Subscription receives the demand and starts working, emits data when the work is done, Subscriber receives data and determine if it need more data by adding demands.
Actors
Actor is also a concrete nominal type like struct, class or enum, it’s created using the actor
keyword.
- actors are reference types
- actors share many of the features of class: properties, methods (async or not), initializers and subscripts; Actors can conform to protocols, and can be generic.
- actors do not support inheritance, so they can’t have convenient initializers, and do not support either
final
oroverride
. - actors automatically conform to
Actor
protocol, which no other type can use. - accessing a variable property or call a method on an actor from outside of hte actor, has to be done asynchronously using
await
.
import UIKit
actor Hero {
var name = "Nameless"
func printIntro() {
print("The hero is \(name).")
}
func visit(_ hero: Hero) async {
print("Visiting another hero.")
await hero.printIntro()
}
}
let hero1 = Hero()
let hero2 = Hero()
Task {
print(await hero1.name)
await hero1.visit(hero2)
}
Output:
Nameless
Visiting another hero.
The hero is Nameless.
- actors are effectively operating a private serial queue taking requests one at a time, in the order they were received.
- only one piece of code at a time can access an actor’s mutable state unless you specifically mark something being unprotected: actor isolation.
- immutable state can be accessed without
await
. - writing properties from outside an actor is not allowed, with or without
await
.
Q: Swift 5.5 introduced modern concurrency features, what are async
, await
, Task
and TaskGroup
?
A:
- async: Indicates that a method or function is asynchronous. Calling it lets you suspend execution until a result is returned from an asynchronous method.
- await: Indicates that the code might pause its execution while it waits for a method or function to return, annotated with
async
. - Task: a unit of asynchronous work, it can be waited to complete or cancelled before it finishes.
Task
is a type that represents a top-level async task, meaning it can create an asynchornous context, that can start from a synchronous context. A newTask
is needed if you want to run asynchronous code from a synchronous context. - TaskGroup: a group that contains dynamically created child tasks.
Swift splits up the code into logical units called partial tasks, or partials, the runtime schedules each of them separately for asynchronous execution.
The TaskGroup
can spawn multiple tasks simutaneously, wait for the completion, and return the result only after all the child tasks have finished execution. The withThrowingTaskGroupfunction
method that does this, requires a result type that specifies the type of the result of the function.
With TaskGroup
:
- All the tasks are independent of each other, and the app cannot control the execution order.
- All the tasks of TaskGroup are concurrent, and they start automatically.
- The TaskGroup returns the result only after all the tasks have completed their execution.
Q: What is DispatchWorkItem
?
A: DispatchWorkItem
encapsulates work to be performed on a dispatch queue or a dispatch group. It is primarily used in scenarios where we require the capability of delaying or canceling a block of code from executing. It lets you cancel the task if the task hasn’t started.
A common use case is searching, you want to delay the search for 0.3 second to make sure user has finished typing all the characters.
var workItem: DispatchWorkItem?
func search(_ term: String) {
workItem?.cancel()
let searchWorkItem = DispatchWorkItem {
// perform search
}
workItem = searchWorkItem
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(300), execute: workItem)
}
notify
schedules the execution of specified work item after the completion of another work item:
func fetchPlayers() {
let fetchWorkItem = DispatchWorkItem {
// fetch players from server
}
let notifyWorkItem = DispatchWorkItem {
// notify the completion of fetching
}
fetchWorkItem.notify(queue: .main) {
notifyWorkItem.perform() // start execution of the work item synchronuously on the current thread
}
DispatchQueue.global().async(execute: fetchWorkItem)
}
wait
causes the caller to wait synchronously until the dispatch work item finishes executing.
let workItem = DispatchWorkItem {}
DispatchQueue.global().async(execute: workItem)
workItem.wait()
DispatchWorkItemFlags defines a set of behaviours for a work item, such as quality-of-service class and whether to create a barrier or spawn a new detached thread. The most commonly used flags are assignCurrentContext
and barrier
.
barrier
causes the work item to act as a barrier block when submitted to a concurrent queue. In a concurrent queue, multiple tasks are executed simultaneously on different threads. When the work item with the barrier flag starts executing, all the tasks in the queue are temporarily suspended and will be resumed once this work item is finished.assignCurrentContext
sets the attributes of the work item to match the attributes of the current execution context.
Q: What are the syntax of async/await
for function, property and closure?
A:
Function: add async
before return type, if the function throws, add async
before throws
:
func someFunction() async throws -> Int {…}
let someValue = try await someFunction()
Computed property: add async
to the getter and access it by prepending await
:
var someProperty: Int {
get async {
…
}
}
let value = await someProperty
Closure: add async
to the signature:
func doSomething(completion: async (Result<Data, Error>) -> Void) {
// …
}
doSomething { result in
await parse(result)
}
Q: What does the runtime do when execution reaches a method called with await
?
A:
- execution of the current code will be suspended.
- the method after
await
will either execute immediately or later, depending on system load and priorities. - if the method or one of its child tasks throws an error, the error will bubble up in the call hierarchy to the nearest
catch
statement.
Q: The following code makes two asynchronous calls, marking them with await
makes the second call won’t happen until the first call finishes. How could you make them happen at the same time?
playerProfiles = try await game.playerProfiles()
playerPhotos = try await game.playerPhotos()
A: Swift offers a special syntax that groups several async calls and await them all together:
do {
async let playerProfiles = try game.playerProfiles()
async let playerPhotos = try game.playerPhotos()
} catch {
// …
}
let (profiles, photos) = try await (playerProfiles, playerPhotos)
Q: What is the syntax in modern Swift concurrency world (ie. async/await
) that runs code on the main thread, equivalent to DispatchQueue.main.async
?
A:
await MainActor.run {…}
To make sure a method is executed on the main actor automatically:
@MainActor func someMethod()
Q: What is AsyncSequence
?
A: AsyncSequence
is a protocol describing a sequence that can produce elements asynchronously, similar to Sequence
, except that the next element need to be await
.
for try await item in asyncSequence {…}
while let item = try await asyncSequence.makeAsyncIterator().next() {…}
Q: Is NSNotification
send asynchronously or synchronously? If a notification is sent from a non-main thread, in which thread it is received?
A: Synchronous. It will be received on the same non-main thread.
Q: When should GCD and NSOperation be used?
A:
For one-off computation, or simply speeding up an existing method, it will often be more convenient to use a lightweight GCD dispatch than employ NSOperation. NSOperation can be scheduled with a set of dependencies at a particular queue priority and quality of service. Unlike a block scheduled on a GCD queue, an NSOperation can be cancelled and have its operational state queried. And by subclassing, NSOperation can associate the result of its work on itself for future reference.
Q: What are the options for converting old async code to modern async/await?
A:
XCode’s Refactor function provides three options to conver async code to modern async/await. For code below:
func performSearch(completion: @escaping (Result<[SearchResult], SearchError>) -> Void) {
guard let url = URL(string: "https://itunes.apple.com/search?term=\(searchText)") else {
completion(.failure(.generic))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { completion(.failure(error)) }
if let data = data {
let response = try? JSONDecoder().decode(SearchResponse.self, from: data)
completion(.success(response?.results ?? []))
return
}
completion(.failure(.generic))
}.resume()
}
performSearch { [weak self] result in
switch result {
case .success(let response):
// …
case .failure(let error):
// …
}
}
Refactor the async function has three options:
- Convert Function to Async
- Add Async Alternative
- Add Async Wrapper
Add Async Wrapper
func performSearch() async throws -> [SearchResult] {
return try await withCheckedThrowingContinuation { continuation in
performSearch() { result in
continuation.resume(with: .result)
}
}
}
The original function has been modified:
@available(*, renamed: "performSearch()")
func performSearch(completion: @escaping (Result<[SearchResult], SearchError>) -> Void) {…}
So now you can use:
Task {
do {
results = try await performSearch()
} catch {
// …
}
}
// instead of
// performSearch { [weak self] result in
// switch result {
// case .success(let response):
// // …
// case .failure(let error):
// // …
// }
// }
Add Async Alternative
func performSearch() async throws -> [SearchResult] {
guard let url = URL(string: "https://itunes.apple.com/search?term=\(searchText)") else {
completion(.failure(.generic))
return
}
return try await withCheckedThrowingContinuation { continuation in
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { completion(.failure(error)) }
if let data = data {
let response = try? JSONDecoder().decode(SearchResponse.self, from: data)
completion(.success(response?.results ?? []))
return
}
completion(.failure(.generic))
}.resume()
}
}
Convert Function to Async
The last option simply converts the old function to an Async one. This is the most disruptive approach that you will have to modify every place where the old function is called.
Further Refactor
func performSearch() async throws -> [SearchResult] {
guard let url = URL(string: "https://itunes.apple.com/search?term=\(searchText)") else {
throw .generic
}
let (data, _) = try await URLSession.shared.data(from: url)
let result = try JSONDecoder().decode(SearchResponse.self, from: data)
return result.results ?? []
}
Language Specific
TODO: Explain Swift vs Objective-C
A:
Both are OO.
Swift
- dynamic libraries supported
- tuples
- semicolons at the end of line not mandatory
- struct
- enum with associated value
Objective-C
- dynamic libraries not supported
- tuples not supported
- use of semicolons is mandatory
- struct not supported
- enum cannot have associated value
Q: How does Swift ensure type safety?
A: Swift doesn’t allow implicit type casting, so you can’t declare something as Int and store a Double value in it.
Q: Why is immutability important?
A: Immutability is like a contract, saying something will never change. So for developers, it allows you not having to worry about what the value is at different stages, especially if the value is unexpected, how and when it was change; For the compiler it can perform certain optimizations, like putting struct
on stack instead of heap, it even gives you warning if something can be let
but is declared as var
.
Q: What is identity operator in Swift?
A: ===
and !==
are the identity operators in Swift, they are only used for object types.
Q: What is the guard
statement?
A: A guard statement is used to transfer program control out of scope. Guard statement is similar to the if statement, but it runs only when some conditions are not met.
Q: Describe how to define a custom operator.
A:
- Declare:
- type: unary or binary
- operator itself
- associativity
- precedence
precedencegroup ExponentPrecedence {
higherThan: MultiplicationPrecedence
associativity: right
}
infix operator **: ExponentPrecedence
- Implementation:
func **(base: Int, exponent: Int) -> Int {
let l = Double(base)
let r = Double(exponent)
let p = pow(l, r)
return Int(p)
}
Q: What is Type Aliasing?
A: Assigning another name to an existing data type.
Q: How do you use existing keyword as variable identifier?
A: By surrounding the variable with single tick mark.
Q: Is a single character in a pair of double quotation mark a Character or String?
A: By default all characters are Strings, you have to declare it as Character to make it a Character.
Q: What’s the computational complexity of String
’s count
?
A:
O(n).
Swift uses extended grapheme clusters for Character values, each can be composed of multiple Unicode scalars, as a result, the number of characters in a string can’t be calculated without iterating through the string to determine its extended grapheme cluster boundaries.
The count of the characters returned by the count property isn’t always the same as the length property of an NSString that contains the same characters. The length of an NSString is based on the number of 16-bit code units within the string’s UTF-16 representation and not the number of Unicode extended grapheme clusters within the string.
Q: Is floating-number Float?
A: By default all floating point values are doubles, use explicit type annotation to make it a Float.
Q: What is ternary operator?
A: Ternary operator operates on three targets, if the first expression is true, it evaluates and returns the value of second expression, otherwise it evaluates and returns the value of the third expression.
Q: How do you define a type that can be initialized with an Int
literal? Say
level1 = Level(1)
level2: Level = 2
A:
Swift offers several protocols that allow you to initialize a type with literal values by using the assignment operator.
public struct Level {
private let level: Int
public init(_ level: Int) {
self.level = level
}
}
extension Level: ExpressibleByIntegerLiteral {
public init(integerLiteral value: IntegerLiteralType) {
self.init(value)
}
}
a: Level = 1
Q: What is the difference between class
and static
properties or functions?
A:
static
properties or functions cannot be overridden, equivalent toclass final
class
properties or functions can be overriden
Q: How does constants in ObjC const
differ from a let
constant in Swift?
A: A const
variable is initialized at compile time with a value or expression that have to be resolved at compile time; A let
variable is a constant determined at runtime, it can be initialized with either a static or dynamic expression.
Q: What is LLVM, LLDB and Clang?
A: The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Clang is a C language front-end for LLVM. LLDB builds on libraries provided by LLVM and Clang to provide a great native debugger.
Q: Explain Semaphore in iOS?
A: DispatchSemaphore
provides an efficient implementation of a traditional counting semaphore, which can be used to control access to a resource across multiple execution contexts.
Q: What is availability
attributes?
A:
It indicates the code should only be called if running system meet the requirement specified.
@available(iOS 9.0, *)
@available(iOS, introduced: 9.0)
@available(OSX, introduced: 10.11)
// is replaced by
@available(iOS 9.0, OSX 10.11, *)
if #available(iOS 9.0, *) {}
Q: What is the difference between Any
and AnyObject
?
A:
- Any can represent an instance of any type at all, including function types and optional types
- AnyObject can represent an instance of any class type
Q: What is property wrapper?
A: A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.
To define a property wrapper, you make a structure, enumeration, or class that defines a wrappedValue property. In the code below, the TwelveOrLess structure ensures that the value it wraps always contains a number less than or equal to 12. If you ask it to store a larger number, it stores 12 instead.
@propertyWrapper
struct TwelveOrLess {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
You can also use generics with property wrappers.
@propertyWrapper
struct UserDefaultsStore<Value> {
let store = UserDefaults.standard
let key: String
var wrappedValue: Value? {
get {
store.value(forKey: key) as? Value
}
set {
store.setValue(newValue, forKey: key)
store.synchronize()
}
}
}
struct Player {
@UserDefaultsStore<String?>(key: "name") var name
@UserDefaultsStore<Int?>(key: "age") var age
}
Property wrapper and closure:
@propertyWrapper
struct MainThread<T> {
var closure: ((T) -> Void)?
var wrappedValue: ((_ data: T) -> Void)? {
get {
{ data in
DispatchQueue.main.async {
closure?(data)
}
}
}
set {
closure = newValue
}
}
}
class ViewModel {
@MainThread var updateUI: ((_ response: Result<ResponseModel, ResponseError>) -> Void)?
func fetchData() {
DispatchQueue(label: "background").async { [weak self] in
guard let self = self else {
return
}
let result = // fetch data
self.updateUI?(result)
}
}
}
With Swift 5.5 we can use property wrapper as arguments:
private func toLowercase(@Lowercased _ str: String) -> String {
str
}
Some limitations of property wrappers:
- only applicable to
var
- only one property wrapper per property
- no inheritance or protocol compliance
- a property with a wrapper cannot be overriden in a subclass
- a property with a wrapper cannot be
lazy
,@NSCopying
,@NSManaged
,weak
orunowned
- a property with a wrapper cannot have custom
get
orset
wrappedValue
,init(wrappedValue:)
andprojectedValue
must have the same level of access control as the wrapper type itself- a property with a wrapper cannot be declared in a protocol or an extension
Q: What is type erasure?
A:
Type erasure is useful when types are long and complex, which is often the case with Combine. For example, AnyPublisher<SomeType, Never>
is a publisher that will provide SomeType
and never throw an error, we don’t care what exactly the publisher is.
Q: What is downcasting?
A:
as
: used for upcasting and type casting to bridged typeas?
: used for safe casting, returnsnil
if failedas!
: used to force casting, crash if failed
When we’re casting an object to another type in Objective-C, it’s pretty simple since there’s only one way to do it. In Swift, though, there are two ways to cast — one that’s safe and one that’s not . as used for upcasting and type casting to bridged type as? used for safe casting, return nil if failed as! used to force casting, crash if failed. should only be used when we know the downcast will succeed.
Q: What is inout
?
A: inout
parameter means if function modifies the parameter as local variable, the passed-in parameter is also modified, it’s like inout
means reference type whereas value type if without inout
.
Q: Differences between internal
, fileprivate
, private
, and public private(set)
?
A:
internal
entities can be used within any source file from defining module but not in any file outsidefile-private
entities can only be used by its own defining source fileprivate
entities can be used only by the enclosing declaration and extensions of the declaration in the same file- default access level is
internal
public
is less restrictive. By making an entity public you make it accessible by other parts of code inside the same module, and to other modules as well.open
is similar topublic
,open
class is accessible and subclassable outside of the defining module, in contrast,public
class is accessible but not subclassable outside of the defining module. Also,open
class andpublic
class member is accessible outside of the defining module, butopen
class member is overridable outside of the defining module whereaspublic
class member is not.
Q: What are Non-Escaping and Escaping Closures?
A: The lifecycle of a non-escaping closure is simple: Pass a closure into a function The function runs the closure (or not) The function returns Escaping closure means, inside the function, you can still run the closure (or not); the extra bit of the closure is stored some place that will outlive the function. There are several ways to have a closure escape its containing function: Asynchronous execution: If you execute the closure asynchronously on a dispatch queue, the queue will hold onto the closure for you. You have no idea when the closure will be executed and there’s no guarantee it will complete before the function returns. Storage: Storing the closure to a global variable, property, or any other bit of storage that lives on past the function call means the closure has also escaped.
Q: What are designated and convenience initializers?
A: Designated initializers are:
- The CENTRAL point of initialization of a class.
- Classes MUST have at least one.
- Is responsible for initializing stored properties.
- Is responsible for calling super init.
Convenience initializers are:
- SECONDARY supporting initializers for a class.
- It can only call a designated initializer that is defined in the same class
- It can also call another convenience initializers defined in the same class
- They are not required, that sort of implied. they are just initializers that we can write for a CONVENIENCE use case
- In a class, They use the keyword “convenience” before the init keyword.
- Finally here are 3 basic rules of class initialization:
Every class must have a designated initializer, if this class inherits from another, the designated initializer is responsible for calling the designated initializer of its immediate superclass.
Classes can have any number of convenience initializers, a convenience initializer must call another initializer from the same class, whether it is a designated initializer or another conveniences initializer.
Convenience initializers must ultimately call a designated initializer.
Q: What does final
do in Swift?
A: Prevents class or method from being overridden
Q: Differences between open
, public
?
A:
- open allows other modules to use and inherit the class,
open
class members can be used as well as overridden. - public only allows other modules to use the public classes and members, public classes cannot be subclassed, nor public member can be overridden.
Q: When a class has more properties than its super class, how could you still use the designated initializer of superclass?
A: override
the designated initializer of the superclass, initialize all properties that the subclass defines before calling super
’s designated initializer.
Q: What are the purposes of required
initializer?
A: Adding required
keyword to an initializer assures that subclasses implement the required initializer. The two common reasons are factory methods and protocols.
Q: What is defer
?
A: defer
keyword provides a safe and easy way to declare a block that will be executed only when execution leaves the current scope.
Defer is usually used to cleanup the code after execution. This might involve deallocating container, releasing memory or close the file or network handle. When put into the routine, code inside defer block is last to execute before routine exits. Routine in this case could refer to a function. Sometimes when function returns there are hanging threads, incomplete operation which needs to cleanup. Instead of having them scattered across function, we can group them together and put in the defer block. When function exits, cleanup code in the defer gets executed.
Q: What is platform limitation of tvOS?
A:
- no browser support, this means the app can’t link out to a web browser for things like OAuth or social media sites
- cannot explicitly use local storage
- app bundle cannot exceeed 4GB
Q: What is ABI?
A:
ABI stands for Application Binary Interface, at runtime, Swift program binaries interact with other libraries through an ABI, it defines many low level details for binary. ABI stability means locking down the ABI to the point that future compiler versions can produce binaries conforming to the stable ABI. It enables binary compatibility between applications and libraries compiled with different Swift versions.
Q: What is KVO?
A: KVO stands for Key-Value Observing and allows a controller or class to observe changes to a property value. In KVO, an object can ask to be notified of any changes to a specific property; either its own or that of another object.
Q: What is KVC?
A: KVC adds stands for Key-Value Coding. It’s a mechanism by which an object’s properties can be accessed using string’s at runtime rather than having to statically know the property names at development time.
Q: What is the difference between Delegate and KVO?
A: Both are ways to have relationships between objects. Delegation is a one-to-one relationship where one object implements a delegate protocol and another uses it and sends messages to it, assuming that those methods are implemented since the receiver promised to comply to the protocol. KVO is a many-to-many relationship where one object could broadcast a message and one or multiple other objects can listen to it and react. KVO does not rely on protocols. KVO is the first step and the fundamental block of reactive programming (RxSwift, ReactiveCocoa, etc.)
Q: What are failable and throwing initializers?
A: Very often initialization depends on external data, this data can exist as it can not, for that Swift provides two ways to deal with this. Failable initializers return nil of there is no data, and let the developer “create” a different path in the application based on that. In other hand throwing initializers returns an error on initialization instead of returning nil.
Q: Can you describe what is conditional conformance and give an example?
A: conditional conformance is a Swift feature introduced in version 4.1, it allows you to automatically conform to certain protocols if all properties in your custom type conform to these protocols.
Q: How could you make your custom type conform to Hashable
?
A:
- implement the
func hash(into hasher: inout Hasher)
- e.g. call combine on the supplied hasher for each value you want to hash
- also conform to
Equatable
Q: What is Codable
?
A: Introduced in Swift 4, the Codable API enables us to leverage the compiler in order to generate much of the code needed to encode and decode data to/from a serialized format, like JSON.
Codable is actually a type alias that combines two protocols — Encodable
and Decodable
. By conforming to either of those protocols when declaring a type, the compiler will attempt to automatically synthesize the code required to encode or decode an instance of that type, which will work as long as we’re only using stored properties that themselves are encodable/decodable.
struct Player: Codable {
var name: String
var age: Int
}
Encode a Player
into Data
:
do {
let player = Player(name: "Doe", age: 19)
let encoder = JSONEncoder()
let data = try encoder.encode(player)
} catch {
print("Failed to encode player: \(error)")
}
Decode a Player
from `Data:
let decoder = JSONDecoder()
let player = try decoder.decode(Player.self, from: data)
Q: How does Codable
work if the field names in data source e.g. JSON do not match the name in data model defined in code?
{
"player_data": {
"full_name": "John Doe",
"user_age": 19
}
}
A: Two ways that have downsides:
- Change the code of data model to match with data source. This may introduce unconventional coding conventions like
full_name
instead ofname
orfullName
; Also, if the project has code quality automation set up, e.g. SwiftLint, it may stop the code from compiling. - Manually perform encoding and decoding. This would require extra code.
Another way is to define a new type that’s specifically used for encoding and decoding:
extension Player {
struct CodingData: Codable {
struct Container: Codable {
var fullName: String
var playerAge: Int
}
var playerData: Container
}
}
extension Player.CodingData {
var player: Player {
return Player(
name: playerData.fullName,
age: playerData.userAge
)
}
}
To decode:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let codingData = try decoder.decode(Player.CodingData.self, from: data)
let player = codingData.player
Q: What is the difference between the Float, Double, and CGFloat data types?
A: Float is always 32-bit, Double is always 64-bit, and CGFloat is either 32-bit or 64-bit depending on the device it runs on, but realistically it’s just 64-bit all the time. In Swift 5.5, the compiler can automatically perform conversion between Double
and CGFloat
values.
Q: What are one-sided ranges and when would you use them?
A: In Swift, you can create a range using closed range operator or the half-open range operator:
let closedRange = 0...9
closedRange.count // 10
let halfOpenRange = 0..<10
halfOpenRange.count // 10
One-sided ranges allow us to skip either the start or end of a range to have Swift infer the starting point for us:
let items = [
"keyboard",
"cable",
"monitor",
"harddrive",
"charger"
]
items[...2] // ["keyboard", "cable", "monitor"]
items[..<2] // ["keyboard", "cable"]
items[2...] // ["monitor", "harddrive", "charger"]
One-sided ranges can be useful when you want to read from a certain portion of a collection, such as if you want to skip the first N elements in an array.
Q: What does it mean when we say “strings are collections in Swift”?
A: This statement means that Swift’s String
type conform to the Collection
protocol, which means String
values have all the methods defined in Collection
protocol, for example, you can loop over characters, count how long the string is, map the characters and so on.
Q: What is UUID and how do you generate it?
A: UUID stands for universally unique identifier, which means if you generate a UUID right now using UUID it’s guaranteed to be unique across all devices in the world. It’s a great way to generate a unique identifier for something you need to reference.
let uuid = UUID().uuidString
Q: What is @autoclosure
and when would you use it?
A: @autoclosure
lets you define an argument that automatically gets wrapped in a closure. It’s usually used when you want to defer the execution of a potentially expensive or unnecessary expression when it’s passed as argument. One example is the assert
function which are triggered only in debug builds, there’s no need to evaluate it in release builds.
Q: What is the difference between self
and Self
?
A: self
refers to the current object the code is runing inside, Self
refers to the current type the code is running inside.
Q: What does targetEnvironment
do?
A: It’s a condition that lets you differentiate between builds that are for physical devices and those that are for a simulated environment.
Q: What are key paths?
A: A key path refers to a property in a type rather than the exact value of that property in one particular instance.
struct Player {
let name: String
let age: Int
}
let players: [Players] = // …
let allNames = players.map(\.name)
Q: When would you use defer
?
A: When you need to delay a piece of code until a function ends. For example, removing a temporary file when the function finishes.
Q: What is a variadic function?
A: Variadic function accepts any number of parameters, written as ...
(three dots):
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
Q: What does the #available
syntax do?
A: This syntax was introduced in Swift 2.0 to allow run-time version checking of features by OS version number. It allows you to target an older version of iOS while selectively limiting features available only in newer iOS versions.
Q: What does CaseIteratable
do?
A: A type that provides a collection of all of its values.
enum Direction: CaseIterable {
case north, south, east, west
}
let directions = Direction.allCases
Q: What is String Interpolation?
A: It’s the process of embedding values inside the String object.
Q: What is Opaque Type and what problem does it solve?
A:
Opaque type is type with a prefix keyword called some
. It allows you to return a value by providing the protocol it conforms to, without exposing the concrete type. This is useful when developing a module where you don’t want to leak the implementation detail. It sounds similar to generic type, but we can often run into a problem where the compiler complains saying the protocol can only be used as a generic constraint because it has Self or associated type requirements. This is because the compiler can’t resolve the type of the value being returned, by adding the keyword some
, we make it an opaque type that allows the compiler to extract additional information about the concrete type.
For example, we have light provider factory that produces object which conforms to LightProviding
:
public protocol LightProviding {
func letThereBeLight() -> String
}
struct DefaultLightProvider: LightProviding {
func letThereBeLight() -> String {
return "Let there be light."
}
}
public struct LightProviderFactory {
public func makeLightProvider() -> DefaultLightProvider {
return DefaultLightProvider()
}
}
We get an error saying:
Method cannot be declared public because its result uses an internal type
because the public factory returns a DefaultLightProvider
which is internal
.
To make it work, we change the factory to return the public protocol:
public struct LightProviderFactory {
public func makeLightProvider() -> LightProviding {
return DefaultLightProvider()
}
}
It works until we have associated type in the LightProviding
protocol:
public protocol LightProviding {
associatedtype Light
func letThereBeLight() -> Light
}
struct DefaultLightProvider: LightProviding {
func letThereBeLight() -> String {
return "Let there be light."
}
}
public struct LightProviderFactory {
public func makeLightProvider() -> LightProviding {
return DefaultLightProvider()
}
}
Protocol ‘LightProviding’ can only be used as a generic constraint because it has Self or associated type requirements
We fix the error by making the return type opaque:
public struct LightProviderFactory {
public func makeLightProvider() -> some LightProviding {
return DefaultLightProvider()
}
}
Q: What is phantom types?
A: Phantom types are a type that doesn’t use at least one its generic parameters – they are declared with a generic parameter that isn’t used in their properties or methods. So types are used as markers, rather than being instantiated to represent values or objects.
enum PaymentType {
enum BPay {}
enum PayID {}
enum BSBAccount {}
}
struct Transaction<Type> {
let amount: Decimal
let payer: Payer
let payee: Payee
}
func confirmBPay(_ transaction: Transaction<PaymentType.BPay>) {}
func confirmBSBAccount(_ transaction: Transaction<PaymentType.BSBAccount>) {}
func confirmPayID(_ transaction: Transaction<PaymentType.PayID>) {}
Even though we don’t use the generic type parameter, Swift does, which means it will treat two instances of our type as being incompatible if their generic type parameters are different. They aren’t used often, but when they are used they help the compiler enforce extra rules on our behalf – they make bad states impossible because the compiler will refuse to build our code.
Q: What are the differences between Some
and Any
keywords?
A:
The some
keyword was introduced in Swift 5.1. It is used together with a protocol to create an opaque type that represents something that is conformed to a specific protocol. When used in the function’s parameter position, it means that the function is accepting some concrete type that conforms to a specific protocol.
The following 3 function signatures are the same:
func sayHi<T: Thing>(to something: T) {}
func sayHi<T>(to something: T) where T: Thing {}
func sayHi(to something: some Thing) {}
When we use the some
keyword on a variable, we are telling the compiler that we are working on a specific concrete type, thus the opaque type’s underlying type must be fixed for the scope of the variable.
Assigning a new instance of the same concrete type to a variable is prohibited by compiler:
var human: some Thing = Human()
human = Human() // Compiler error
let things: [some Thing] = [
Human(),
Human(),
Dog() // Compiler error
]
// Compiler error
func giveMeSomething() -> some Thing {
if likeHuman {
return Human()
} else {
return Animal()
}
}
The any
keyword was introduced in Swift 5.6 for the purpose of creating an existential type. It’s mandatory in Swift 5.7.
func sayHi(to something: any Thing) {}
// No compiler error in Swift 5.7
func giveMeAnything() -> any Thing {
if likeHuman {
return Human()
} else {
return Animal()
}
}
// No compiler error in Swift 5.7
let things: [any Thing] = [
Human(),
Human(),
Dog()
]
The limitation of any
is you can’t use ==
to compare two instances of the existential type.
let t1 = giveMeAnything()
let t2 = giveMeAnything()
let areTheyTheSame = t1 == t2 // Compiler error
Objective-C
Q: What is the difference between _
vs self.
in Objective-C?
A: You typically use either when accessing a property in Objective-C. When you use _, you’re referencing the actual instance variable directly. You should avoid this. Instead, you should use self. to ensure that any getter/setter actions are honored.
In the case that you would write your own setter method, using _ would not call that setter method. Using self. on the property, however, would call the setter method you implemented.
Q: What are blocks in Objective-C?
A: Blocks are a language-level feature of Objective (C and C++ too). They are objects that allow you to create distinct segments of code that can be passed around to methods or functions as if they were values. This means that a block is capable of being added to collections such as NSArray or NSDictionary. Blocks are also able to take arguments and return values similar to methods and functions.
The syntax to define a block literal uses the caret symbol(^).
Q: What is Method Swizzling?
A: Method swizzling allows the implementation of an existing selector to be switched at runtime for a different implementation in a classes dispatch table. Swizzling allows you to write code that can be executed before and/or after the original method. For example perhaps to track the time method execution took, or to insert log statements.
#import "UIViewController+Log.h"
@implementation UIViewController (Log)
+ (void)load {
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
SEL viewWillAppearSelector = @selector(viewDidAppear:);
SEL viewWillAppearLoggerSelector = @selector(log_viewDidAppear:);
Method originalMethod = class_getInstanceMethod(self, viewWillAppearSelector);
Method extendedMethod = class_getInstanceMethod(self, viewWillAppearLoggerSelector);
method_exchangeImplementations(originalMethod, extendedMethod);
});
}
- (void) log_viewDidAppear:(BOOL)animated {
[self log_viewDidAppear:animated];
NSLog(@"viewDidAppear executed for %@", [self class]);
}
@end
Q: What is the difference between Array
and NSArray
?
A: Array
is a struct, thus it’s value type in Swift, NSArray
is an immutable ObjectiveC class, it is reference type and bridged to Array<AnyObject>
in Swift.
Q: What is the difference between iVar and @property
A: iVar is like the backing variable for declared @property
, @synthesize
d property will have backing iVar created, @property
is used with KVO, and can be overwritten with custom getter and setter.
Q: What is selector in Objc?
A: In Objective-C, selector has two meanings. It can be used to refer simply to the name of a method when it’s used in a source-code message to an object. It also, though, refers to the unique identifier that replaces the name when the source code is compiled. Compiled selectors are of type SEL. All methods with the same name have the same selector. You can use a selector to invoke a method on an object—this provides the basis for the implementation of the target-action design pattern in Cocoa.
Q: Explain the difference between atomic and nonatomic synthesized properties?
A:
atomic
: ensures integrity of setter and getter, safer thannonatomic
, any thread access atomic object can get the correct objectnonatomic
: doesn’t ensure integrity of setter and getter, might be in problem state when different threads trying to access the same object, faster thanatomic
Q: What is the difference between strong
, weak
, readonly
and copy
?
A:
strong
: the reference count will be increased and the reference to it will be maintained through the life of the objectweak
: referencing an object without increasing its reference count, often used when creating a parent child relationshipreadonly
: a property can be read but not modifiedcopy
: the value of an object is copied when it’s assigned, prevents its value from being modified
Q: What is @dynamic
in ObjC?
A: @dynamic
tells the compiler that getter and setter are implemented somewhere else, examples like subclasses of NSManagedObject
.
Q: What is @synthesize
in ObjC?
A:
Synthesize generates getter and setter methods for property, there are a few special cases:
- readwrite property with custom getter and setter when providing both a getter and setter custom implementation, the property won’t be automatically synthesized
- readonly property with custom getter when providing a custom getter for a readonly property, this won’t be automatically synthesized
@dynamic
when using@dynamic propertyName
, the property won’t be automatically synthesized- properties declared in a
@protocol
when comforming to a protocol, any property the protocol defines won’t be automatically synthesized - properties declared in a category
- overriden property when you override a property of a superclass, you must explicitly synthesize it
Q: By calling performSelector:withObject:afterDelay:
is the object retained?
A: Yes, the object is retained. It creates a timer that calls a selector on the current threads run loop. It may not be 100% precise time-wise as it attempts to dequeue the message from the run loop and perform the selector.
Q: What happens when you call autorelease on an object?
A: When you send an object a autorelease message, its retain count is decremented by 1 at some stage in the future. The object is added to an autorelease pool on the current thread. The main thread loop creates an autorelease pool at the beginning of the function, and release it at the end. This establishes a pool for the lifetime of the task. However, this also means that any autoreleased objects created during the lifetime of the task are not disposed of until the task completes. This may lead to the taskʼs memory footprint increasing unnecessarily. You can also consider creating pools with a narrower scope or use NSOperationQueue with itʼs own autorelease pool. (Also important – You only release or autorelease objects you own.)
Q: What’s the output of the following code:
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, weak) NSString *weakString;
strongString = [NSString stringWithFormat:@"%@",@"string1"];
weakString = strongString;
strongString = nil;
NSLog(@"%@", weakString);
A:
NSString
created bystringWithFormat
is autoreleased, there is a delay in releasing it, so the output may not benull
.- In 64-bit system, when a string’s length is less than or equal to 9, the type of string will be
NSTaggedPointerString
, if it has non-ASCII characters, the type would be__NSCFString
. - if the string is initialized via
initWithString:
, it will be of type__NSCFConstantString
.
Q: Are atomic
properties thread-safe?
A: It’s only safe for its getter and setter, not safe in other cases like if you have an atomic
mutable array and you add objects to it.
Q: What is KVO?
A: Key-value observing is a Cocoa programming pattern you use to notify objects about changes to properties of other objects. It’s useful for communicating changes between logically separated parts of your app—such as between models and views. You can only use key-value observing with classes that inherit from NSObject.
Q: What are the steps involved to expose Objective-C class to Swift?
A:
First we need a bridging header file which allows us to define which ObjC files we want to expose to Swift. XCode automatically asks if you’d like to create bridging header file when you add the first Swift file in Objective-C project, or first Objective-C file in Swift project. You can also create the bridging header manually.
Suppose you have an Objective-C code, you have have a different name for Swift:
NS_SWIFT_NAME(ObjcClass)
@interface MyObjcClass: NSObject
@property (strong, nonatomic) NSString *_Nonnull name;
- (void)doSomething;
- (NSString *_Nullable)getSomethingIfAvailable;
@end
Then import this header in the bridging header file:
#import "MyObjCClass.h"
Q: What are the steps involved to expose Swift file to Objective-C?
A:
- prefix
@objc
to class, method or property that we want to expose to Objective-C - the class need to be inherit from
NSObject
- in the Objective-C file, import a header called “[ProductName]-Swift.h”
class MySwiftClass: NSObject {
@objc
let prefix = "Mr"
@objc
func doSomething() {}
}
enum
Exposing Swift enums to Objective-C requires the enum not having associated values and have to be interger enumeration.
objc(XYZPaymentType)
enum PaymentType: Int {
case cash
case debitCard
case creditCard
}
When enum has associated value:
enum CardType: String {
case debit
case credit
}
enum PaymentType {
case cash
case card(CardType)
var rawValue: String {
switch self {
case .cash: return "Cash"
case .card(let cardType): return "Card(\(cardType.rawValue))"
}
}
}
@objcmembers class XYZPaymentType: NSObject {
static let cash = PaymentType.cash.rawValue
static func card(cardType: String) -> String {
PaymentType.card(CardType(rawValue: cardType)!).rawValue
}
}
Error Handling
Q: What are the ways to handle runtime error in Swift?
A:
In Swift you can define custom Error
types, they can use an enumeration that conforms to the Swift error protocol. Methods and initializers can have the throws
keyword in the signature to indicate the particular method or initializer can throw
error; Errors thrown by the method or initializer propogate to the caller, caller can wrap the code in a try…catch
block to catch and handle the error.
Q: What do assert
and precondition
do and what are the differences?
A: They both evaluate, and if the result is false, they both crash the app. The difference is assert
is only active in debug mode, whereas precondition
is also active in release mode. As a general rule, assert
is used for checking your own code for internal errors and precondition
is for checking if the business rule are satisfied, for example, if the arguments are valid to proceed.
Q: Explain rethrows
vs throws
A:
throws
: used to indicate a function, method or initializer can throw an errorrethrows
: indicate a function or method could throw an error only if one of its function parameters throws an error, it’s for functions that do not throw errors on their own, but only forward errors from their function parameters
Q: Explain how could you make your custom Error
bridging to NSError
(with proper errorDomain
, errorCode
and errorUserInfo
)?
A: Make your custom Error
implement the CustomNSError
protocol.
Q: What’s the difference between try?
and try!
for calling a throwing function?
A: try?
prevents the propogation of errors, calling a throwing function with try?
doesn’t care about the reason for failure if it throws; try!
asserts that an error won’t occur, like force unwrap, either it works or you get a crash.
Q: Can you name one or more downsides of how Swift handles errors?
A: Functions that are marked as throwing doesn’t reveal which errors can be thrown.
Q: What is Result
type in Swift?
A: Result
type is an enum with two cases: a success case and a failure case:
public enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
It’s often used in asynchronous code where you get a result asynchronously and the result can be either a value or an error. You can send the result around and handle it at a later stage.
Q: What does map
and mapError
do on Result
?
A: map
transform the result’s success value if present, for example, if you have a Result<Data, NetworkError>
type, and a variable of this type, if its value is success, calling map
you can turn it into Result<JSON, NetworkError>
type, transforming the successful Data
value into a JSON string; If the variable has an error value, calling mapError
you can map the failure value using the given transformation and return a new result, for example, you can transform network error into business domain error.
Q: What does Result(catching:)
do?
A: It creates a new result by evaluating a throwing closure, capturing the returned value as a success, or any thrown error as a failure.
Networking
Q: What are the main components of a HTTP URL? What purpose do they have?
A:
Every HTTP URL consists of the following components:
scheme://host:port/path?query
- Scheme — The scheme identifies the protocol used to access a resource, e.g. http or https.
- Host — The host name identifies the host that holds the resource.
- Port — Host names can optionally be followed by a port number to specify what service is being requested.
- Path — The path identifies a specific resource the client wants to access.
- Query — The path can optionally by followed by a query to specify more details about the resource the client wants to access.
var components = URLComponents()
components.scheme = "https"
components.host = "www.test.com"
components.path = "/request/path"
components.queryItems = [URLQueryItem(name: "q", value: "swift"),
URLQueryItem(name: "quantity", value: "100")]
let url = components.url
// https://www.test.com/request/path?q=swift&quantity=100
Q: What is URLSession
?
A: URLSession
is responsible for sending and receiving HTTP requests, created via URLSessionConfiguration
Q: Difference between WKWebView
and UIWebView
?
A:
UIWebview
:
- part of
UIKit
, no need to import additional module - can scale page to fit
WKWebView
:
- need to import
WebKit
framework - higher and more efficient performance
- runs outside of app’s main process
- supports server-side authentication challenge
WKWebViewConfiguration
provides more custom options
Q: What is OAuth?
A: OAuth is an open standard authorization protocol or framework that describes how unrelated servers and services can safely allow authenticated access to their assets without sharing the initial, related, single logon credential. One simple example is when you go to log onto a website, it offers a few ways to log on using another website’s/service’s logon.
Q: What are the different ways of showing web content to users?
A:
UIWebView
andWKWebView
(UIWebView
is deprecated)
UIWebView
is part ofUIKit
,WKWebView
is part ofWebKit
WKWebView
runs in a separate process to the app, it loads web pages faster and more efficient with less memory overhead- Requests made by
UIWebView
go throughNSURLProtocol
whereWKWebView
doesn’t
SFSafariViewController
provides a visible standard interface for browsing the webUIApplication
has a method that opens an URL.
Q: What are the differences between NSCache
and URLCache
?
A:
NSCache
is an in-memory mutable collection to temporarily store key-value pairs. It’s like an NSMutableDictionary
that automatically frees up space in memory as needed, as well as more thread-safety and conforms to NSCopying
protocol. NSCache
uses system’s memory and will allocate a proportional size to the size of data. Until other applications need memory and system forces this app to minimize its memory footprint by removing some of its cached objects. Though, NSCache
doesn’t guarantee that the purge process will be in orderly manner. Moreover, the cached objects won’t be there in next run. The main advantages of NSCache
are performance and auto-purging feature for objects with transient data that are expensive to create.
URLCache
is both in-memory and on-disk cache, and it doesn’t allocate a chunk of memory for it’s data. You can define it’s in-memory and on-disk size, which is more flexible. URLCache
will persist the cached data until the system runs low on disk space. For any network data management we should use URLCache
rather than NSCache
for caching any data.
URLCache.shared = {
let cacheDirectory = (NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0] as String).appendingFormat("/\(Bundle.main.bundleIdentifier ?? "cache")/" )
return URLCache(memoryCapacity: /*your_desired_memory_cache_size*/,
diskCapacity: /*your_desired_disk_cache_size*/,
diskPath: cacheDirectory)
}()
Q: In iOS, a networking feature called App Transport Security (ATS) requires that all HTTP connections use HTTPS. Why?
A:
HTTPS (Hypertext Transfer Protocol Secure) is an extension of HTTP. It is used for secure computer network communication. In HTTPS, the communication protocol is encrypted using TLS (Transport Layer Security).
The goal of HTTPS is to protect the privacy and integrity of the exchanged data against eaves-droppers and man-in-the-middle attacks. It is achieved through bidirectional encryption of communications between a client and a server.
So through ATS, Apple is trying to ensure privacy and data integrity for all apps. There is a way to bypass these protections by setting NSAllowsArbitraryLoads
to true in the app’s Information Property List file. But Apple will reject apps who use this flag without a specific reason.
Q:
Build a simply networking layer.
A:
protocol Endpoint {
var scheme: String { get }
var baseURL: String { get }
var path: String { get }
var parameters: [URLQueryItem] { get }
var method: String { get }
}
final class Service {
class func request<ResponseType: Codable>(endpoint: Endpoint, completion: @escaping (Result<ResponseType, Error>) -> Void) {
var components = URLComponents()
components.scheme = endpoint.scheme
components.host = endpoint.baseURL
components.path = endpoint.path
components.queryItems = endpoint.parameters
guard let url = components.url else {
return
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method
let session = URLSession(configuration: .default)
let dataTask = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard response != nil, let data = data else { return }
DispatchQueue(label: "background").async {
if let responseObject = try? JSONDecoder().decode(ResponseType.self, from: data) {
completion(.success(responseObject))
} else {
let error = NSError(domain: "ServiceDomain", code: 200)
completion(.failure(error))
}
}
}
dataTask.resume()
}
}
enum PaymentEndpoint: Endpoint {
case payToMobile
case payToBSBAccount
case payBill(billerCode: String, referenceCode: String)
var scheme: String {
"https"
}
var baseURL: String {
"www.xyzpayment.com"
}
var path: String {
switch self {
case .payToMobile:
return "payToMobile"
case .payToBSBAccount:
return "payToBSBAccount"
case .payBill:
return "payBill"
}
}
var parameters: [URLQueryItem] {
switch self {
case .payToMobile, .payToBSBAccount:
return []
case let .payBill(billerCode, referenceCode):
return [URLQueryItem(name: "biller", value: billerCode),
URLQueryItem(name: "reference", value: referenceCode)]
}
}
var method: String {
"GET"
}
}
Architecture
Q: What are the common problems that a poor architecture may have?
A:
- readability: the codebase is hard to understand
- not easy to change: adding or updating one small feature requires changing lots of code, which often causes regressions
- fragile product: chronical bugs that are not always reproducible
- reuseability: code not easy to reuse
- maintainability: code not to maintain - developers working on different areas end up modifying same code
- unit test: test coverage is poor due to code not easy to be unit tested
- story delivery: the team often finds it hard to break stories into technical tasks
- locked into a technology: hard to move to latest technology, e.g.
NSURLConnection
andNSURLSession
- difficult to apply feature toggles
The problems above is a result of not sticking to SOLID principles, which often manifests as highly interdependent code and long types.
Some considerations when solving the above problems:
refactor large classes by adding dependencies
Break up large classes into a number of smaller classes, the original large class becomes smaller and dependent on the smaller classes that used to be part of the original class.
remove duplicate code
make duplicate code a reuseable component
Q: Explain polymorphism?
A:
Polymorphism is about provisioning a single interface to entities of different types.
Polymorphism allows the expression of some sort of contract, with potentially many types implementing the contract in different ways, each according to their own purpose.
Q: What is dependency injection?
A: It’s a set of software design principles and patterns that enables loosely coupling, basically an object receives other instances it depends on, commonly done by requiring implementations that each conforms to certain protocol. It can be done via initializer, or set as properties of the object. Often, if the dependency doesn’t need to live longer than the object that requires it, the object can instantiate as a default value for its initializer argument.
Dependency injection provides the following benefits:
- avoid large classes by having the class dependent on the instances of smaller classes, making it easier to conform to the single responsibility principle
- mocking data easily for unit tests, you create mock objects conforming to the protocols that are required to create the tested object, and in the mock objects you make it suitable to perform the intended tests.
- you know all dependencies are configured at compile time
There are things to be careful with DI:
- Do you have to change everywhere in your code to enable DI? If so, this usually means your classes have to wrap and pass around the dependencies through a long route before they’re actually used.
- Do you have to inject all dependencies via initializers? If so, you may have messy code which create a number of dependencies and sub-dependencies, and long initializers everywhere.
Q: What is Dependency Inversion Principle?
A: DI decouples classes from one another by explicitly providing dependencies for them, rather than having them create the dependencies themselves.
Q: Why is design pattern important?
A: Design patterns are reusable solutions to common problems in software design. They are templates designed to help you write code that’s easy to understand and reuse.
Q: What is Singleton pattern?
A:
The singleton pattern ensures that only one instance exists for a given class and that there’s a global access point to that instance. It usually uses lazy
loading to create the single instance when it’s needed for the first time.
Q: What is Facade pattern?
A:
The facade pattern provides a single interface to a complex subsystem. Instead of exposing the user to a set of of classes and their APIs, only a simple unified API is exposed.
Q: What is Decorator pattern?
A:
The decorator pattern dynamically adds behaviours and responsibilities to an object without modifying its code. It’s an alternative to subclassing where you modify a class’s behavior by wrapping it with another object.
In ObjC two common implementations are Category and Delegation, in Swift there are Extension and Delegation.
Q: What is Adapter pattern?
A:
An adapter allows classes with incompatible interfaces to work together, it wraps itself around an object and expose a standard interface to interact with that object.
Protocol A makes request, B is a class that makes special request, adapter implements A and wraps a B instance, its request implementation would call B’s special request.
Q: What is Observer pattern?
A:
In observer pattern, one object notifies other objects of any state changes.
Q: What is Memento pattern?
A:
In memento pattern, your stuff is saved somewhere external, later when needed, externalised state can be restored without violating encapsulation.
Q: What is VIPER?
A:
View, Interactor, Presenter, Entity and Router:
View
: UI related operations, similar to View in MVVM, represents eitherUIView
orUIViewController
, user interaction is passed to PresenterPresenter
: similar to ViewModel in MVVM, Presenter is responsible for taking user inputs from the view, and transferring them to the interactor. Presenter changes the type of the data, if the Interactor requires the data in a specific format. In practice the modules communicate through the presenters.Router
: Screen transition and UI components switching. The router has to have a direct reference to the viewcontroller, with the presenter acting as an intermediary between view and router.Interactor
: Data processing, including network request, data storage and modelling. Interactor bidirectionally interacts with the presenter to receive inputs, fetch data, perform complex calculations, the results of which are displayed to the user through the interactor. The Interactor can communicate with a DataManager (the component responsible for fetching data from the network), based on the translated user input fed by the Presenter. In practice the Interactor can get data from a webservice.Entity
: Data model
Pros:
- structure of app is more modular, separation of concerns, conform to single responsibility principle
- reduce the work and dependency on controllers
- easier to unit test
Cons:
- communication between different objects in VIPER can be complicated
- for small applications some object e.g. Router may be just a placeholder
Implementation
- view controller creates router
- router:
- looks after views
- creates presenter and binds presenter to view
- creates interactor and binds interactor with presenter
- interactor informs presenter when data is fetched
Q: What is the RIBS pattern used by Uber?
A:
RIBs is Uber’s cross-platform architecture framework. This framework is designed for large mobile applications that contain many nested states.
The main idea of this architecture is that the app should be driven by business logic, and not by the view.
During the app lifecycle, RIBs can be attached and detached, create the child nodes, and interact with them.
RIBs stands for “Router Interactor Builder”.
- router is responsible for navigation between adjacent RIBS
- interactor is the main component, it handles the RIB business logic. It reacts to user interactions, talks to a backend and prepares data that will be displayed to the user
- builder is a constructor that builds together all the RIB pieces
- optional view rendering UI and passes user interaction to interactor. The interactor owns the view and the view talks to interactor via delegate pattern
- optioanl presenter is basically a protocol that the view implements
Example: on the confirm payment screen in an app that has payment.
- user taps on the “Confirm” button on payment confirmation screen (View)
- the view informs the interactor about the confirmation
- interactor then tells the presenter to show an activity indicator, also sends the request to confirm payment
- upon payment success, the interactor informs the router, which then navigates user to the receipt screen
Q: What is delegation pattern?
A: The delegation pattern is a powerful pattern used in building iOS applications. The basic idea is that one object will act on another object’s behalf or in coordination with another object. The delegating object typically keeps a reference to the other object (delegate) and sends a message to it at the appropriate time. It is important to note that they have a one to one relationship.
Q: What is factory pattern?
A: Factory Method is used to replace class constructors, to abstract and hide objects initialization so that the type can be determined at runtime, and to hide and contain switch/if statements that determine the type of object to be instantiated.
Q: What is MVC?
A: MVC stands for Model-View-Controller. It is a software architecture pattern for implementing user interfaces.
MVC consists of three layers: the model, the view, and the controller.
- The model layer is typically where the data resides (persistence, model objects, etc)
- The view layer is typically where all the UI interface lies. Things like displaying buttons and numbers belong in the view layer. The view layer does not know anything about the model layer and vice versa.
- The controller (view controller) is the layer that integrates the view layer and the model layer together.
Q: What is MVVM pattern?
A:
The MVVM defines the following:
- The View, which is generally passive, corresponds to the Presentation Layer
- The Model, corresponds to the Business Logic Layer and is identical to the MVC pattern
- The View Model sits between the View
- The Model, corresponds to the Application Logic Layer
The key aspect of the MVVM pattern is the binding between the View and the View Model. In other words, the View is automatically notified of changes to the View Model.
In iOS, this can be accomplished using Key-Value-Observer (KVO) Pattern. In the KVO Pattern (https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html) , one object is automatically notified of changes to the state of another object. In Objective C, this facility is built into the run-time. However, it’s not as straightforward in Swift. One option would be to add the “dynamic” modifiers to properties that need to be dynamically dispatched. However, this is limiting as objects now need to be of type NSObject. The alternate is to simulate this behavior by defining a generic type that acts as a wrapper around properties that are observable.
Q: What is MVP pattern?
A:
The MVP defines the following:
- The View, which is generally passive, corresponds to the Presentation Layer
- The Model, corresponds to the Business Logic Layer and is identical to the MVC pattern
- The Presenter sits between the View and the Model, corresponds to the Application Logic Layer. A View is typically associated with one Presenter.
In iOS, the interaction between the View and Presenter can be implemented using the Delegation Pattern (https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html). The Delegation Pattern allows one object to delegate tasks to another object. In iOS, the pattern is implemented using a Protocol. The Protocol defines the interface that is to be implemented by the delegate.
In the MVP pattern, the View and Presenter are aware of each other. The Presenter holds a reference to the view it is associated with.
The PresenterProtocol defines the base set of methods that any Presenter must implement. Applications must extend this Protocol to include application specific methods.
protocol PresenterProtocol: class{
func attachPresentingView(_ view: PresentingViewProtocol)
func detachPresentingView(_ view: PresentingViewProtocol)
}
The PresentingViewProtocol defines the base set of methods that View must implement. By providing default implementation of the methods in this interface, the conformant view does not have to provide its own implementation. This interface can be extended to define application specific methods.
protocol PresentingViewProtocol: class{
func dataStartedLoading()
func dataFinishedLoading()
func showErrorAlertWithTitle(_ title: String?, message: String)
func showSuccessAlertWithTitle(_ title: String?, message: String)
}
Q: What is coordinator pattern and what problem does it solve?
A:
The coorindator pattern decouples the application’s navigation responsibility away from the view controller in a self contained module making them more reusable and easier to test. Coordinators only keep navigation logic between screens, and/or references to a storage that has data used by factories for creating modules.
The main goal is to have unchained module-to-module responsibility and to build modules completely independently from each other.
A coorindator may consists of the following items:
- router is used for navigation
- factory for creating modules and injecting all dependencies
- (optional) coordinator’s factory in case we need to switch to a different flow
- (optional) storage if we store and data that can be injected into modules
struct PaymentCoorindator {
private let router: Router
private let factory: PaymentFactory
}
extension PaymentCoordinator {
func showRecipientList() {
let recipientList = factory.makeRecipientList()
recipientList.onSelectRecipient = { [weak self] recipient in
self?.showRecipientDetail(recipient)
}
recipientList.onCreateRecipient = { [weak self] in
self?.showCreateRecipientScreen()
}
router.setRoot(recipientList)
}
}
Q: What is Redux pattern and what problem does it solve?
A:
Redux is the implementation of an architectural software pattern that prioritizes unidirectional data flow. Created from the Flux architecture (developed by Facebook), it has grown considerably in the development of applications and promises great advantages in its use. It is an alternative to other architectural patterns such as: MVC, MVVM and Viper.
It has the following components:
- State: represents the state of the application, there must be one only, and can be divided into sub-states.
- Actions: Objects that describe what the system can do, dispatched by the View layer as intentions to change the state of the application.
- Reducers: the main logic of the application, they are given an action and the current state, and return a new state.
- Store: aggregates all the components and make the flow work.
View dispatches a new action to the store, the store then passes this action to reducer along with the current state and then receives a new state back from reducer; The view is then informed when a new state is created, this can be implemented by using the Observer pattern where view is a subscriber of the store to be notified.
Q: Can you give some examples of where singletons might be a good idea?
A: When there’s something that can have a single instance exist at any time, and it doesn’t makes sense to have multiple instances of it, for example, UIApplication
. Although, direct use of UIApplication
can be avoided by having specific protocol for the functionalities in UIApplication
and refer to the protocol instead of UIApplication
’s shared instance, this way you can have dependency injection and it’s also useful for unit testing.
Q: What are the SOLID principles?
A:
S - Single-responsibility principle
A class should have one and only one reason to change, meaning that a class should have only one job.
O - Open-Closed Principle
Objects or entities should be open for extension but closed for modification.
A class should be extendable without modifying the class itself.
L - Liskov Substitution Principle
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
The principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of your subclasses to behave in the same way as the objects of your superclass.
I - Interface Segregation Principle
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
The Interface Segregation Principle (ISP) states that a client should not be exposed to methods it doesn’t need. Declaring methods in an interface that the client doesn’t need pollutes the interface and leads to a “bulky” or “fat” interface.
D - Dependency Inversion Principle
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
The Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
Q: In achitecture design, why is a unidirectional data flow so important?
A: It allows you to clearly reason about your code. In the massive view controller problem, the VC handles all the logic for the application. By defining clear interfaces between layers of the system, and requiring all data flow in the same direction, it’s easy to understand where state is handled and easy to troubleshoot problems.
Q: In architecture design, why is immutability so important?
A: Immutable data structures help prevent a whole lot of state related problems. Immutability simplifies state management across thread so you don’t have to lock mutable states; Prevents accidental state manipulation, and helps enforcing unidirectional data flow by not allowing layers to “cheat” and manipulate existing data structures.
XCode Project
Q: What are the pros and cons of cocoapods and Swift package manager?
A:
Cocoapods
- supported by more frameworks so far?
- pod try lets you test before integrate
- first time integration takes time to clone the Specs repository, also happens when
pod update
although not the whole repo - changes the project file
- all the dependencies will be built when building the project
SPM
- Apple’s standard way of managing packages
- Automatically manages dependencies
- Has to follow a precise folder structure
Q: Library vs Framework?
A:
Libraries are files that define pieces of code and data that are not a part of your Xcode target.
The process of merging external libraries with app’s source code files is known as linking. The product of linking is a single executable file that can be run on a device.
Based on how libraries are linked to the app, there are two categories: static and dynamic (another category exists also: text Based .dylib stubs — .tbd).
Object files have Mach-O format which is a special file format for iOS and macOS operating systems. It is basically a binary stream with the following chunks:
Header: Specifies the target architecture of the file. Since one Mach-O contains code and data for one architecture, code intended for x86-64 will not run on arm64.
Load commands: Specify the logical structure of the file, like the location of the symbol table. Raw segment data: Contains raw code and data.
Mach-O files support single architecture, lipo
tool can be used to package multiple libraries into a universal one, called fat binary, or vice-versa.
Framework is a package that can contain resources such as dynamic libraries, strings, headers, images, storyboards etc. With small changes to its structure, it can even contain other frameworks. Such aggregate is known as umbrella framework.
Frameworks are also bundles ending with .framework extension. They can be accessed by NSBundle / Bundle class from code and, unlike most bundle files, can be browsed in the file system that makes it easier for developers to inspect its contents. Frameworks have versioned bundle format which allows to store multiple copies of code and headers to support older program version.
Q: Static library vs dynamic library?
A:
Static Library
Libraries that are copied as part of the executable file when it is generated, when app is launched, the app including linked static libraries is loaded into the app’s address space.
Pros:
- guaranteed to be present in the app and have correct version
- no need to keep an app up to date with library updates
- better performance of library calls
Cons:
- large app size, slow launch time and large memory footprint
- when static library is updated, the apps that have been released don’t benefit from the update, developers have to link the app’s object files with updated version of the library and users have to update the app to the latest version.
Dynamic Library
Dynamic libraries are not statically linked to the client apps, not part of the executable file. They can be loaded and linked into an app either when the app is launched or as it runs.
Pros:
- reduced launch time and memory footprint compared to static library
- the functionality of the app can be improved or extended without requiring app develops to recompile the app
- can be initialized when they are loaded and can perform clean-up tasks when the client app terminates
Cons:
- one thing to keep in mind is that, with dynamic libraries, the developers need to maintain the compatibility with the apps as the libraries update, because a library can be updated without the knowledge of the app’s developer and the app must be able to use the new version of the library without changes to its code
- can be slow when calling library functions as it’s located outside of app executable
Q: What can Cocoapod do with linking libraries?
A:
By default, CocoaPods builds and links all the dependencies as static libraries; You can add use_frameworks!
to Podfile
to let Cocoapod build and link the dependencies as dynamic frameworks; You can also use specify use_frameworks! :linkage => :static
to make Cocoapod build and link dependencies as static frameworks.
Q: What are the problems with Git submodule?
A:
- It requires a good understanding of how Git workk. e.g. make sure submodule is pushed separately before pushing the parent repo, checkinng in a bad reference to a submodule blocks other people from using it.
- Updating repos to point to a new version of submodule require updating all repos that uses the submodule to point to the latest version.
- Git clone doesn’t clone submodule by default, you need
git submodule update
orgit clone --recursive
- Git submodule may be out-of-sync if someone updates its and you forgot to specifically pull it.
- Switching branches in a repo with submodules is a pain.
App Submission
Q: What is “app ID” and “bundle ID”?
A:
An App ID is a two-part string used to identify one or more apps from a single development team. The string consists of a Team ID and a bundle ID search string, with a period (.) separating the two parts. The Team ID is supplied by Apple and is unique to a specific development team, while the bundle ID search string is supplied by the developer to match either the bundle ID of a single app or a set of bundle IDs for a group of apps.
The bundle ID defines each App and is specified in Xcode. A single Xcode project can have multiple targets and therefore output multiple apps. A common use case is an app that has both lite/free and pro/full versions or is branded multiple ways.
Before code signing app during distribution, XCode automatically prefixes the bundle ID with team ID - the unique character sequence issued by Apple to each development team - and stores the combined string as the app ID. Bundle ID unique identifies an application in Apple’s ecosystem.
Q: What is bitcode?
A: Bitcode refers to the type of code: “LLVM Bitcode” that is sent to iTunes Connect, this allows Apple to use certain calculations to re-optimize apps further and this can be done without uploading a new build.
Slicing is the process of Apple optimizing app for user’s specific device App Thinning is the combination of slicing, bitcode and on-demand resources
Q: What is the purpose of code signing in Xcode?
A: It’s useful for verifying developer identity to make sure the app shipped is safe, also it validates the functionalities enabled by the provisioning profile.