Getting your Trinity Audio player ready...
|
Preparing for an iOS interview requires you to take considerable amount of time to review Swift language features. Try to skim as many following questions as possible, you might not remember every possible question but at least heard about it.
- Difference between callAsFunction and @dynamicCallable
The main difference between callAsFunction
and @dynamicCallable
is that the former is more static and type-safe, while the latter is more dynamic and flexible. The primary use case for @dynamicCallable
is to interoperate with dynamic languages like Python and JavaScript, while callAsFunction
can be used to create types that behave like functions or closures.
- How to call an instance of a type as if it were a function?
The callAsFunction
method is a special method that allows you to call an instance of a type as if it were a function. For example, you can define a struct with a callAsFunction
method that takes some arguments and returns a value:
struct Adder {
var base: Int
func callAsFunction(_ x: Int) -> Int {
return x + base
}
}
let add3 = Adder(base: 3)
let result = add3(10) // result is 13
This is equivalent to calling the callAsFunction
method explicitly:
let result = add3.callAsFunction(10) // result is 13
- How to define dynamic member lookup types?
Swift attribute @dynamicMemberLookup
enables you to access properties of a type that are not declared at compile time, but resolved at runtime. You need to implement a subscript method subscript(dynamicMember:)
that takes a string or a Keypath as an argument and returns any value you like.
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["name": "Taylor Swift", "city": "Nashville"]
return properties[member, default: ""]
}
}
let taylor = Person()
print(taylor.name) // Taylor Swift
print(taylor.city) // Nashville
print(taylor.favoriteIceCream) // ""
- How to define dynamically “callable” types?
The @dynamicCallable
is an attribute in Swift that allows an object to be called like a function. This means that you can use the object’s name followed by parentheses with arguments inside, just like you would call a function. You need to define either of following methods, or both, in your type (struct, class, enum, protocol):
dynamicallyCall(withArguments:)
: This method is called when you use the object like a function. It receives an array of arguments and should return a value.dynamicallyCall(withKeywordArguments:)
: This method is called when you use the object like a function with named arguments. It receives a dictionary of arguments and should return a value.
Here’s an example of how to use @dynamicCallable
in Swift:
@dynamicCallable
struct MyCallable {
func dynamicallyCall(withArguments args: [Int]) -> Int {
return args.reduce(0, +)
}
}
let callable = MyCallable()
let result = callable(1, 2, 3, 4) // equivalent to callable.dynamicallyCall(withArguments: [1, 2, 3, 4])
print(result) // prints 10
- Why is Swift a type-safe language?
Swift is a type-safe language, which means that it helps you avoid errors by checking the types of values and expressions in your code. Swift performs type checks when compiling your code and flags any mismatched types as errors. This prevents you from passing a value of the wrong type to a function, variable, constant, or property by mistake.
var age = 25 // age is inferred to be an Int
age = "Twenty five" // error: cannot assign value of type 'String' to type 'Int'
- What are Swift optionals?
Swift optionals let you indicate the absence of a value for any type at all, without the need for special constants. Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.
func foo(bar: String?) {
print(bar ?? "default value if missing")
}
The concept of optionals doesn’t exist in Objective-C. The nearest thing in Objective-C is the ability to return nil
from a method that would otherwise return an object, with nil
meaning the absence of a valid object.
- Difference between assertions and preconditions?
func foo(bar: Int) {
assert(bar > 1, "bar must be greater than 1")
/* ... */
}
Known from many programming languages, the assert()
is evaluated only in debug mode, will terminate application if condition not met. For release builds, lines with assert()
are ommited (condition is not evaluated).
func foo(bar: Int) {
precondition(bar > 1, "bar must be greater than 1")
/* ... */
}
The precondition()
ensures that the given condition is checked for both debug and release builds, will terminate the application if condition not met. For unchecked optimization level (-Ounchecked
) optimizer will assume that condition is always met.
- Why immutability is important in Swift?
Immutability helps safety and performance. Immutable objects are useful in multi-threaded applications because multiple threads can act on the data of immutable objects without worrying about changes to the data by other threads.
As immutable objects are closed to change, it is safe to assume that they will stay unchanged while we access the object from different threads. This assumption simplifies most of the multithreading problems that are complex to solve and maintain. For instance, we do not need to think about synchronization/locking mechanisms at all.
- Why were C-style for-loops removed in Swift 3?
let items = ["foo", "bar"]
for (var i = 0; i < items.count; i += 1) {
print(items[i])
}
The C-style for-loop
appears to be a mechanical carry-over from C rather than a genuinely Swift-specific construct. It is rarely used and not very Swift-like.
More Swift-typical construction is already available with for-in
statements and stride. Removing for loops would simplify the language and starve the most common use-points for --
and ++
, which are already due to be eliminated from the language.
let items = ["foo", "bar"]
for item in items {
print(item)
}
for i in 0..<items.count {
print(items[i])
}
for (index, item) in items.enumerate() {
print("\(index): \(item)")
}
- What are types of closures?
Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.
Closures can capture and store references to any constants and variables from the context in which they’re defined. Closures take one of three forms:
- Global functions are closures that have a name and don’t capture any values.
- Nested functions are closures that have a name and can capture values from their enclosing function.
- Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.
- Difference between structures and classes?
Structures and classes are general-purpose, flexible constructs that have many things in common.
- Define properties to store values.
- Define methods to provide functionality.
- Define subscripts to provide access to their values using subscript syntax.
- Define initializers to set up their initial state.
- Be extended to expand their functionality beyond a default implementation.
- Conform to protocols to provide standard functionality of a certain kind.
Classes have additional capabilities that structures don’t have:
- Inheritance enables one class to inherit the characteristics of another.
- Type casting enables you to check and interpret the type of a class instance at runtime.
- Deinitializers enable an instance of a class to free up any resources it has assigned.
- Reference counting allows more than one reference to a class instance.
- How to choose between value types and reference types?
The most basic distinguishing feature of a value type is that copying — the effect of assignment, initialization, and argument passing — creates an independent instance with its own unique copy of its data.
Copying a reference, on the other hand, implicitly creates a shared instance. After a copy, two variables then refer to a single instance of the data, so modifying data in the second variable also affects the original.
One of the primary reasons to choose value types over reference types is the ability to more easily reason about your code. Importantly, you can safely pass copies of values across threads without synchronization.
When you’re working with Cocoa, many APIs expect subclasses of NSObject
, so you have to use a class. For the other cases, here are some guidelines:
Use a value type when:
- Comparing instance data with
==
makes sense - You want copies to have independent state
- The data will be used in code across multiple threads
Use a reference type (e.g. use a class
) when:
- Comparing instance identity with
===
makes sense - You want to create shared, mutable state
- Difference between
@objc
vsdynamic
?
class Foo {
func bar() -> Int {}
@objc func bar1() -> Int { /* ... */ }
@objc dynamic func bar2() -> Int { /* ... */ }
}
A function/variable declared as @objc
is accessible from Objective-C, but Swift will continue to access it directly via static or virtual dispatch. This means if the function/variable is swizzled via the Objective-C framework, like what happens when using Key-Value Observing or the various Objective-C APIs to modify classes, calling the method from Swift and Objective-C will produce different results.
Using dynamic
tells Swift to always refer to Objective-C dynamic dispatch. When the Swift function is called, it refers to the Objective-C runtime to dynamically dispatch the call. You might need this for KVO support or if you‘re doing method swizzling. The only way to do dynamic dispatch currently is through the Objective-C runtime, so you must add @objc
if you use dynamic
.
- What is
@escaping
in Swift?
func foo(_ completion: @escaping () -> Void) {
DispatchQueue.main.async {
completion()
}
}
@escaping
is used to inform callers of a function that takes a closure that the closure might be stored or otherwise outlive the scope of the receiving function. This means that the caller must take precautions against retain cycles and memory leaks. It also tells the Swift compiler that this is intentional.
- Difference between
@objc
and@nonobjc
?
When you use the @objc(name)
attribute on a Swift class, the class is made available in Objective-C without any namespacing. As a result, this attribute can also be useful when migrating an archivable Objective-C class to Swift. Because archived objects store the name of their class in the archive, you should use the @objc(name)
attribute to specify the same name as your Objective-C class so that older archives can be unarchived by your new Swift class.
Conversely, Swift also provides the @nonobjc
attribute, which makes a Swift declaration unavailable in Objective-C. You can use it to resolve circularity for bridging methods and to allow overloading of methods for classes imported by Objective-C. If an Objective-C method is overridden by a Swift method that cannot be represented in Objective-C, such as by specifying a parameter to be a variable, that method must be marked @nonobjc
.
- How to find dynamic type of a value?
The static type of a value is the known, compile-time type of the value. The dynamic type of a value is the value’s actual type at run-time, which can be a subtype of its concrete type.
You can use the type(of:)
function to find the dynamic type of a value, particularly when the dynamic type is different from the static type.
func printInfo(_ value: Any) {
let t = type(of: value)
print("'\(value)' of type '\(t)'")
}
let count: Int = 5
printInfo(count) // '5' of type 'Int'
The dynamic type returned from type(of:)
is a concrete metatype (T.Type
) for a class, structure, enumeration, or other nonprotocol type T
, or an existential metatype (P.Type
) for a protocol or protocol composition P
.