How to Answer iOS Interview Questions Like a Pro 👩🏽‍💻👨🏻‍💻 (free training course)

Vincent Pradeilles
12 Dec 202335:08

Summary

TLDR本视频课程由Revenue Cat赞助,向观众免费提供,旨在教授如何在技术iOS面试中给出卓越的答案。课程首先介绍了一个简单的框架,帮助面试者以结构化、清晰和有影响力的方式回答问题。随后,通过10个不同的示例,展示了如何将这一框架应用于常见的iOS技术面试问题。课程的目标是使观众能够在下一次iOS面试中有效地应用这一框架,从而提高回答问题的能力。整个过程旨在减轻面试时的压力,确保面试者能够自信且有效地展示他们的技术能力。

Takeaways

  • 😀 通过Revenue Cat的赞助,这个原本是付费的iOS面试技巧培训课程现在可以在YouTube上免费观看。
  • 📱 课程介绍了一个简单的框架,帮助应聘者以结构化、清晰和有影响力的方式回答面试问题。
  • 🔍 框架分为三个步骤:首先是通过重新阐述问题和明确假设来给答案提供上下文;其次是提供实际答案;最后是总结答案的关键点。
  • 🤓 通过10个不同的例子展示了如何应用这个框架来回答典型的iOS技术面试问题。
  • 👨‍💻 对于“可选项”的问题,提供了详细的答案,包括如何通过使用问号来使变量成为可选项,以及如何安全地处理可能为nil的值。
  • 🛠 解释了@escaping和non-@escaping闭包的区别,以及如何根据闭包的使用场景选择合适的类型。
  • 🏗 讨论了计算属性与方法的选择,强调了从语义角度出发选择两者之一的重要性。
  • 🔑 强调了使用Swift关键字`guard`可以改善代码可读性,通过早期返回来简化错误处理路径。
  • ⚠️ 讨论了在Swift中使用高级语言特性可能成为坏实践的情况,例如不恰当地使用`defer`关键字。
  • 💡 探讨了`weak`和`unowned`关键字的使用,以及它们如何帮助避免内存泄漏。
  • 📦 解释了`@autoclosure`属性的用途,特别是在可能不需要立即计算表达式的情况下。
  • 🛠️ 推荐了几种iOS开发中常用的开源工具,如SwiftLint、SwiftFormat和Fastlane。
  • 📲 讨论了如何选择iOS应用的最低支持版本,以及这个决定如何平衡技术和商业考虑。
  • 🏛 最后,解释了依赖注入的概念,以及如何通过使用协议和初始化器参数来提高代码模块化。

Q & A

  • 什么是可选类型(optional)?它的作用是什么?

    -可选类型(optional)是Swift中处理可能为nil的值的类型安全机制。它通过在类型后添加问号来定义,表示该类型可以允许nil值。可选类型的优点是它会强制我们明确处理nil值,从而避免出现空指针异常。我们可以通过可选链式调用和可选绑定等安全方式来处理nil值,或者通过强制解包的不安全方式处理,但后者可能会导致崩溃。

  • 逃逸闭包(escaping closure)和非逃逸闭包(non-escaping closure)有什么区别?

    -非逃逸闭包的生命周期限制在它被传入的函数内,不会对内存安全构成威胁。而逃逸闭包会逃逸所在函数的生命周期,可能会产生循环引用,需用@escaping显式标注。在逃逸闭包中要特别小心强引用self,最好使用捕获列表捕获weak self避免循环引用。

  • 在什么情况下应该使用计算属性,什么情况下应该使用方法?

    -计算属性用于封装不产生副作用、幂等的获取值的逻辑,而方法更灵活,可以用于任何逻辑。如果计算属性的使用场景不满足语义要求,应当改用方法,否则容易产生错误。

  • guard的目的是什么?它如何改善代码的可读性?

    -guard用于实现条件不满足时的提前返回,使代码的快乐路径成为主流程,错误路径从主流程分支出去,提高了可读性。但在guard条件中使用双重否定时需要小心,可能产生误导。

  • 选择一个iOS应用的最低支持版本时,需要考虑哪些因素?

    -考虑技术限制(低版本缺少新API)、业务范围(是否需要面向更多用户)、发布时间等因素。这是一个技术和业务的权衡取舍。

Outlines

00:00

📱 iOS面试指南开篇

本视频由Revenue Cat赞助,原为付费课程现免费提供。视频介绍了一种简单框架,用于结构化、清晰、有影响力地回答iOS面试问题。通过10个示例展示如何应用这一框架回答典型的iOS技术面试问题,旨在帮助观众在iOS面试中有效运用此框架。

05:01

🔍 探索Optional

介绍了Optional在Swift中的概念,它是处理可能为nil值的类型安全方式。通过比较安全和不安全的处理Optional的方法,强调了优先选择安全方法以避免程序崩溃的重要性。同时,指出Optional是Swift标准库的一部分,通过enum实现,虽然我们可以查看其GitHub上的实现代码,但编译器内置的语法糖是不可自行实现的。

10:01

🔄 理解@escaping和@nonescaping闭包

讨论了闭包在Swift中的两种类型:@escaping和@nonescaping。解释了闭包如何捕获值以及这种捕获机制如何导致内存泄漏。通过区分这两种闭包类型,强调了如何正确使用闭包来避免内存泄漏,特别是在使用@escaping闭包时需要额外小心。

15:01

🛠️ 计算属性与方法的选择

探讨了何时应使用计算属性而不是方法的问题。强调了计算属性应遵守的语义规则,如无副作用、幂等性和恒定时间执行。如果代码不符合这些规则,则应选择使用方法。这段讨论旨在指导开发者如何根据代码的行为选择最合适的封装方式。

20:05

🔑 Swift关键字guard的用途

讲解了Swift中guard关键字的用途,特别是在改善代码可读性方面的价值。通过将错误路径早早地从主流程中分离出来,guard能够让正常路径的代码结构更为清晰。同时,讨论了使用guard时需要注意的事项,如避免在条件中使用否定形式以减少读者的理解负担。

25:05

📦 探讨defer的正确使用

分析了Swift中defer关键字的使用场景,强调了它在处理对称操作时的价值,如打开/关闭文件或启动/停止进程。同时指出,不恰当的使用defer可能会导致代码更加混乱,不建议在不适合的场合使用defer,以免引入不必要的复杂性。

30:06

🔗 弱引用与无主引用的区别

详细解释了Swift中weak和unowned引用的定义及其使用场景。强调了在避免循环引用和内存泄漏时选择正确引用类型的重要性。讨论了weak和unowned引用的性能差异,建议在不需要极端优化的情况下优先考虑使用weak引用以确保更高的代码安全性。

🚀 at auto-closure属性的应用

讨论了at auto-closure属性在Swift中的作用,尤其是在优化代码执行效率而不牺牲可读性的场景中的价值。通过自动将表达式包装在闭包中,可以延迟执行代价高昂的计算。同时警告了使用at auto-closure可能导致的潜在问题,如在调用点不明显的情况下引入副作用。

🛠️ 推荐的开源iOS工具

列举了几种iOS开发中常用的开源工具,如SwiftLint、SwiftFormat、Fastlane、SwiftGen和Sorcery,以及它们各自的用途。讨论了如何根据项目的实际需求审慎选择这些工具,以提高项目质量而不是无谓地增加复杂度。

📱 确定iOS应用的最低支持版本

讨论了决定iOS应用支持的最低版本的重要性,包括技术和商业考量。强调了如何根据应用类型、目标用户群和技术需求来决定支持的iOS版本,旨在找到业务需求和工程选择之间的平衡点。

🏗️ 依赖注入的概念与应用

介绍了依赖注入(DI)的概念,解释了它如何通过解耦组件的创建和使用来增加代码的模块化。讨论了通过协议和构造器参数实现DI的方法,以及这种方法如何促进更灵活的代码重用和测试。最后强调了DI的简单性,尽管它的名称听起来可能很复杂。

Mindmap

Keywords

💡面试问题

视频讲解了如何回答iOS技术面试中的问题。面试问题是评估应聘者技能和知识的一种方式。视频提供了一个简单的3步框架来回答面试问题,使答案更清晰、结构化。

💡上下文化

在回答面试问题时的第一步。重述问题,明确任何假设。这有助于面试官纠正任何错误假设,也让面试官有时间进入聆听模式。

💡解答问题

回答面试问题的第二步。直接回答面试官的问题,提供所需的解释、定义或最佳实践。

💡总结答案

回答面试问题的第三步。概括答案的关键点,这可以让答案更结构化,也展示对该主题的更深入理解。

💡可选类型

一个变量在Swift中可以选择支持nil值。可选类型通过在类型后添加?来表示,它通过Optionals枚举实现。可选类型可以更安全地处理潜在的空值。

💡闭包

一段可以在其他地方执行的代码。闭包会捕获(capture)外部变量的强引用,可能导致循环引用。通过@escaping标记,我们可以明确闭包的生命周期可能超出函数调用范围。

💡计算属性

封装产生某个值的代码的方式之一。与方法类似,但有更严格的要求——计算属性不应该有副作用,应该是幂等的。只有当这些要求完全满足的时候,才应该使用计算属性。

💡guard语句

一种语法糖,通过提前退出函数、方法来简化代码结构。guard语句可以让“快乐路径”成为代码主流,错误处理成为分支。但在条件中使用双重否定可能会使代码难以理解。

💡弱引用

不持有实例的强引用的一种方式,通常用来避免循环引用。与unowned引用相比,weak引用由于要处理实例被释放的情况,是可选类型,处理起来更安全。

💡最小支持版本

iOS应用需要决定最低支持的iOS系统版本。这个选择影响可以使用的API和框架,也与用户群体相关。需要在技术可行性与业务目标之间找到平衡。

Highlights

提出了一个简单的三步框架来帮助回答面试题目

第一步是概括问题,澄清任何假设

第二步是提供答案,解释或定义

最后一步是总结答案的要点

Optional是Swift处理潜在空值的类型安全机制

非逃逸闭包不会造成内存泄漏

计算属性与方法在技术上非常相似

guard关键字可以让代码更具可读性

当语法糖的要求没有完全满足时最好不要使用它

尽管defer很有用,但在其他上下文中使用它可能是一个坏习惯

weak引用处理其他实例被回收的情况

at自动闭包允许延迟评估参数

开源工具可以提高项目质量

最小iOS版本的选择需要平衡商业和工程之间的权衡

依赖注入通过解耦来提高代码的模块性

Transcripts

play00:00

Welcome to this video where you'll learn how

play00:02

to give great answers to technical iOS interview

play00:05

questions.

play00:06

The content you're about to see used to be

play00:08

a paid training course, but thanks to the generous

play00:10

sponsorship of Revenue Cat, it is now available

play00:13

for free on YouTube.

play00:15

So I have a big thank you for them, and if

play00:17

you're curious to see how they can help you

play00:19

integrate in-app purchases into your iOS app,

play00:23

you have a link to learn more in the description.

play00:25

That's all I wanted to say, enjoy the video.

play00:28

Hey, welcome to this training course to learn

play00:30

how to answer iOS interview questions like a pro.

play00:33

So what's the plan for this course?

play00:35

It's pretty straightforward.

play00:37

First, I'm going to show you a simple framework

play00:40

that you can apply to answer interview questions

play00:42

in a way that feels structured,

play00:44

clear, and impactful.

play00:46

And then I will show you through 10 different

play00:49

examples how to apply this framework to answer

play00:52

typical questions that you could expect in

play00:54

an iOS technical interview.

play00:56

And my goal is that by the end of this course,

play00:59

you will be able to efficiently apply this

play01:01

framework the next time you're taking an iOS interview.

play01:05

Answering an interview question can definitely

play01:07

be stressful.

play01:08

Someone is trying to evaluate you, you're trying

play01:11

to get a job, so the stakes are definitely high.

play01:14

And in that situation,

play01:15

it's very easy to stumble,

play01:17

be a bit confused, and basically deliver your

play01:20

answer in a way that feels subpar.

play01:23

To try and avoid this, I want to show you a

play01:25

very simple framework in three steps that will

play01:29

help you deliver your answer in a way that feels clear,

play01:32

structured,

play01:33

and impactful.

play01:34

So let's see what are these three steps.

play01:36

The first step is to start by contextualizing

play01:40

your answer.

play01:41

What do I mean by contextualize?

play01:43

I mean that you should reformulate the question

play01:45

and explicitly state any assumptions you're making.

play01:48

You can see this as an introduction for your answer.

play01:51

It's a way to nicely ease into the actual answer

play01:54

and also give a bit of time to the interviewer

play01:57

to fully switch into listening mode.

play02:00

But this is also a great way for the interviewer

play02:02

to correct you if for some reason you've made

play02:05

a wrong assumption.

play02:06

This way you don't waste time starting to deliver

play02:09

an answer that's going into the wrong direction.

play02:12

So once you're done contextualizing, it's time

play02:14

to move on to the next step, which is to deliver

play02:17

the actual answer.

play02:19

Here it's relatively straightforward, you just

play02:21

answer the question, meaning that you give

play02:23

either the explanation,

play02:25

the definition,

play02:26

the best practice that the interviewer is expecting

play02:29

to hear.

play02:30

But after you're done answering the question,

play02:32

it's not over, there is still one last step,

play02:35

which is to recap your answer.

play02:38

The goal here is to sum up the key points of

play02:41

your answer.

play02:42

This shouldn't take too long, just a few sentences,

play02:45

but doing so will really help make your answer

play02:47

feel more structured.

play02:49

And if you have more knowledge on the topic

play02:51

or on adjacent or related topics, this is also

play02:54

a great time to mention it.

play02:56

Don't forget that the goal of the interviewer

play02:58

is to evaluate your skill level in general,

play03:01

and the questions are just a proxy to achieve that goal.

play03:06

And so any extra information you can share

play03:08

that will help convey this feeling that you

play03:11

do know your stuff will only have a positive impact.

play03:15

So these are the three steps of this simple framework,

play03:18

contextualize, answer,

play03:19

and finally recap.

play03:21

Now it's time to see this framework in action

play03:23

to see how we can apply this framework to an

play03:26

actual iOS interview question.

play03:29

So let's apply our framework to answer a first

play03:32

interview question.

play03:33

And that question is,

play03:35

what's an optional?

play03:37

Back when I was interviewing candidates for

play03:39

junior to mid-level positions, I would actually

play03:42

always start the interview by asking this question

play03:45

because I feel it's a nice simple question

play03:48

and it's a great way to get the interview started.

play03:52

So let's use our framework to answer this question.

play03:55

The first step is to contextualize the question.

play03:58

Here a great way to contextualize is to remind

play04:01

the interviewer that one of the goals of Swift

play04:04

is to be a safe programming language.

play04:07

This means that unlike in other languages,

play04:11

variable by default cannot store a null value.

play04:15

This means that if you have a variable that

play04:17

for instance is typed with int,

play04:19

then the compiler will expect that variable

play04:22

to hold an actual int and it won't be possible

play04:26

to set a null value to the variable.

play04:29

But of course there are situations where it

play04:31

makes sense for a variable to actually hold

play04:33

a null value and so Swift does offer a mechanism

play04:37

to make a variable nullable and we opt in to

play04:41

this mechanism by making the type of the variable

play04:45

be an optional.

play04:47

That's more than enough for the contextualization.

play04:49

Now let's move on to the actual answer.

play04:52

We can start by stating that to make a variable

play04:55

be an optional,

play04:57

we add a question mark to the type of the variable

play05:00

which is nothing more than a shortcut for using

play05:04

the generic type optional of t.

play05:06

And by making a variable optional,

play05:09

the compiler will actually make sure that we

play05:12

explicitly deal with the potential nil value.

play05:17

This is the big advantage of optional.

play05:19

It offers a type safe approach to dealing with

play05:23

values that can potentially be nil.

play05:26

And we have basically two ways to deal with

play05:29

the potential nil value.

play05:30

First we can use the safe approach which is

play05:34

by using syntaxes like optional chaining to

play05:37

call a method on an optional or conditional binding,

play05:41

the famous if let syntax.

play05:44

And these approaches allow us to deal with

play05:46

the optional in a way that is safe, meaning

play05:49

that if the value is actually nil our code

play05:52

won't crash but we will need to write a bit

play05:55

more code to deal with the potential nil case.

play05:59

And then we have unsafe approaches like for

play06:02

instance force unwrapping.

play06:04

These approaches will require a bit less syntax

play06:07

because we don't need to deal with the potential

play06:11

nil value but they have one big drawback which

play06:14

is that if the value turns out to be nil then

play06:17

your code will crash.

play06:18

So of course most of the time we do want to

play06:20

use the safe way of handling an optional because

play06:23

we don't want our code to crash when there

play06:26

is actually a nil value.

play06:28

And unsafe approaches should really be used

play06:31

with a lot of caution and should be reserved

play06:34

to the rare cases where we are extremely sure

play06:38

that the value will never be nil and we should

play06:41

never lose sight of the fact that if for some

play06:44

reason, for some unforeseen reason, the value

play06:47

turns out to be nil then our program will crash.

play06:51

So that's what a potential answer to this question

play06:53

looks like.

play06:54

Now let's move on to the last part of our framework,

play06:57

the recap.

play06:58

Here the recap is pretty straightforward.

play07:01

We can just state that optional is Swift's

play07:04

type safe mechanism to deal with potentially nil values.

play07:09

And don't forget that the recap is also a great

play07:12

time to share any extra knowledge that you

play07:14

have on the topic.

play07:15

For instance, here you can share the fact that

play07:18

optional is actually implemented in Swift itself.

play07:22

It's part of the standard library and you can

play07:24

go see its code on GitHub.

play07:27

And optional is actually implemented using

play07:30

an enum with two cases,

play07:32

none and then some with an associated value

play07:35

to store the actual value.

play07:37

However, while this means that we could basically

play07:41

implement our own version of optional,

play07:44

we couldn't implement everything because most

play07:47

of the sugar syntax that makes optional quite

play07:50

easy to use in Swift is itself implemented

play07:53

in the compiler.

play07:55

So that's all for this first question.

play07:56

Now let's move on to the second one.

play07:58

So for this second question, I picked a topic

play08:01

that is slightly more complex and it's the

play08:03

difference between at escaping and non-at escaping

play08:07

closures.

play08:08

So let's see how we can apply our framework

play08:10

to this new question.

play08:12

First, we start by once again contextualizing

play08:14

the question.

play08:15

Here a great way to contextualize is to give

play08:18

a simple definition of what a closure is.

play08:22

So something along these lines, a closure is

play08:24

a piece of code that we write in one place,

play08:27

but that is meant to be executed at a later time.

play08:30

And then we move on to the fact that because

play08:33

they will be executed later,

play08:35

closures need to make sure that the values

play08:38

they interact with will still be in memory

play08:40

by the time they are executed.

play08:43

And as we know, this ability of closures to

play08:46

capture strong references is a double-edged

play08:49

sword because it also opens up the way to memory

play08:53

leaks through retained cycles.

play08:55

I think that's good enough for the contextualization.

play08:57

Now we can move on to delivering the actual answer.

play09:01

So first, we can start by stating that not

play09:04

all closures are meant to be used the same way.

play09:08

We can really split closures into two different kinds.

play09:12

First, we have the closures that are not meant

play09:14

to have a lifetime that exceeds the one of

play09:17

the function that they are passed to.

play09:19

A typical example of such closures are the

play09:22

ones you pass to functions like map or filter.

play09:25

These closures are meant to be called inside

play09:27

the body of the function that you pass them

play09:30

to, but once that function has returned, they

play09:33

no longer serve any purpose.

play09:35

And these are the closures that we call non

play09:38

-escaping closures.

play09:39

The name reflects the fact that these closures

play09:42

don't escape the scope of the function they've

play09:46

been passed to.

play09:46

And because their lifetime is limited to that

play09:50

of the function they've been passed to, they

play09:52

don't pose a threat regarding memory safety

play09:55

because they cannot create a memory leak since

play09:58

their lifetime is limited.

play10:01

This means that inside a non-escaping closure,

play10:04

we don't actually need to worry about capturing

play10:07

a strong reference because it's just not possible

play10:10

for such a closure to lead to a retained cycle.

play10:13

And in Swift, closures are actually non-escaping

play10:17

by default.

play10:18

However, some closures do have the legitimate

play10:21

need to outlive the function that they've been

play10:24

passed to.

play10:25

Think for instance of the closure that handles

play10:27

the result of a network call.

play10:29

Such a closure will be executed after that

play10:32

the function that started the network call has returned.

play10:35

Such closures are called escaping closures

play10:38

because they escape the lifetime of the function

play10:42

they've been passed to.

play10:43

And in Swift, they must be explicitly annotated

play10:46

with the attribute at escaping.

play10:49

And inside such closures, we must be very careful

play10:52

whenever we capture a strong reference to an

play10:55

instance because these are the kind of closures

play10:58

that have the potential for creating a retained cycle.

play11:02

However, Swift does provide us with the tools

play11:05

to capture references in safe manners inside

play11:09

such closures by using the closures capture list.

play11:13

A typical example of this is when we capture

play11:15

a weak reference to self inside a closure that

play11:18

will be directly or indirectly also be stored in self.

play11:23

I think it's more than enough for the answer.

play11:25

Now we can move on to the recap.

play11:28

So for the recap, we can just state that closures

play11:31

are known to be one of the top cause of memory

play11:34

leaks in iOS.

play11:36

But not all closures have the potential to

play11:39

create such memory leaks.

play11:41

Only closures that escape the lifetime of the

play11:44

function they're passed to can potentially be harmful.

play11:48

And that's why Swift forces us to explicitly

play11:51

annotate such closures with at escaping.

play11:54

But you want to be careful because there does

play11:57

exist some rare instances where at escaping

play12:01

will actually be implicit.

play12:03

An example of that is when you pass an optional

play12:06

closure as an argument.

play12:08

In that situation, the at escaping attribute

play12:11

won't be explicitly written,

play12:13

but the closure will still behave like an escaping

play12:16

closure.

play12:17

That's all for this second question.

play12:19

Let's move to the next one.

play12:20

So with this new question, we're going into

play12:22

the topic of good practices and code readability.

play12:26

The question is when would you use a computed

play12:28

property versus a method?

play12:31

Once again, let's apply our framework to answer

play12:34

this question.

play12:35

We begin by contextualizing our answer.

play12:38

So here, a great way to contextualize the answer

play12:41

is to just reformulate the question.

play12:44

So we can state that in Swift, when we want

play12:47

to encapsulate a piece of code that produces a value,

play12:51

we basically have two potential choices.

play12:54

We can put that piece of code either inside

play12:57

a computed property or inside a method.

play13:00

And the interesting thing here is that from

play13:03

a technical point of view, computed properties

play13:06

and methods are actually quite similar.

play13:09

When you think about it, a computed property

play13:11

is actually nothing else than a method with

play13:15

no argument and a simplified syntax to call it.

play13:18

And because of that, the choice between the

play13:20

two options cannot really be decided from the

play13:22

technical point of view, but rather should

play13:25

be decided from a semantics point of view,

play13:27

meaning we should consider what the code actually does.

play13:30

Because in Swift, properties are expected to

play13:33

have some specific semantics.

play13:35

They shouldn't have side effects, meaning that

play13:38

calling a computed property should not produce

play13:41

a side effect in another part of our code.

play13:44

They should also be idempotent, which is a

play13:47

mathematical word to say that if we call the

play13:50

properties several times successively in a

play13:53

row, it should always return the same value.

play13:57

So for instance, a property shouldn't be used

play13:59

to implement a random value because getting

play14:02

a random value is definitely not idempotent.

play14:05

The entire idea is to get a different value every time.

play14:09

And finally, properties should execute in constant time.

play14:13

So in terms of time complexity,

play14:15

they should be O of 1, meaning that if for

play14:19

instance you call a computed property on a collection,

play14:22

the time the property will take to execute

play14:24

should not depend on the size of the If all

play14:28

these properties are met, then it's absolutely

play14:31

okay to encapsulate our code to produce a value

play14:35

using a computed property.

play14:38

However,

play14:39

if not all of them are met, then it's better

play14:42

to use a method because the code we have doesn't

play14:46

behave like we expect a property to behave.

play14:50

Because if we were to still encapsulate the

play14:52

code inside a computed property, it's basically

play14:55

guaranteed to generate trouble at some point

play14:57

when another person will use the property and

play15:01

will expect it to behave in a way that it doesn't.

play15:04

So that was a big answer.

play15:06

Now let's move on to the recap.

play15:08

So for this recap, we can conclude by stating

play15:11

that syntax sugar, like compute properties,

play15:14

can be really useful to write shorter code.

play15:17

However,

play15:18

we must be really careful to only use syntax

play15:22

sugar when it actually makes sense.

play15:25

And it's usually a bad idea to try to use a

play15:28

syntax sugar when not 100% of its requirements

play15:32

have been met because it's basically guaranteed

play15:35

to lead to trouble at some point.

play15:38

So for this fourth question, I chose something

play15:40

that is still on the topic of code readability

play15:42

by asking what is the purpose of the Swift

play15:46

keyword guard?

play15:47

Once again, let's apply our framework to answer

play15:50

this question.

play15:51

So how can you contextualize this question?

play15:54

You can start by saying that when we implement

play15:57

a feature in an iOS app, we usually split our

play16:01

code into two parts.

play16:03

First, we have what we call the happy path,

play16:05

meaning the code that implements our feature

play16:07

assuming that everything is working correctly.

play16:11

And then we also have the error paths, which

play16:15

deal with all the potential technical and business

play16:18

errors that can happen.

play16:20

So think of things like having no network connection,

play16:23

having an expired authentication token,

play16:26

using an invalid promo code, that kind of things.

play16:29

And ideally, we would like the structure of

play16:32

our code to be such that the happy path is

play16:34

the main flow of the code and the error paths

play16:37

are branching out of this main flow.

play16:40

However, if we implement our code using if

play16:43

statements to make sure that all the conditions

play16:46

are met, it's actually the opposite that will

play16:49

be happening.

play16:50

What will happen is that each new error path

play16:52

and so each new if statement will cause the

play16:55

happy path to become more and more deeply nested.

play16:58

And as we can imagine, this is when the keyword

play17:01

guard comes into play.

play17:03

Guard will allow us to reverse the structure

play17:05

of the code by implementing an early return

play17:08

from the function whenever a condition has not been met.

play17:13

And so by using guard, we will be able to refactor

play17:16

our code into a much more readable structure

play17:19

with the happy path at the center, like we

play17:23

initially wanted.

play17:24

So that was the answer.

play17:25

Now let's finish with the recap.

play17:27

And for the recap, we can simply state that

play17:30

guard is the typical example of when seemingly

play17:33

simple sugar syntax actually has the potential

play17:37

of dramatically improving the readability of our code.

play17:41

However, like with all sugar syntax, we want

play17:44

to be careful because they always come with a flip side.

play17:48

In the case of guard, we want to be careful

play17:50

when using a negation inside the condition

play17:52

because then the statement ends up being a

play17:55

double negation, which can be hard to read

play17:58

and potentially confusing.

play18:00

For this next question, I picked a much more

play18:02

open topic by asking to give an example of

play18:06

a bad practice in Swift.

play18:08

So once again, let's answer the question using

play18:10

our framework and start by contextualizing the question.

play18:14

Here we can contextualize by stating that a

play18:17

bad practice is usually a situation where what

play18:20

initially felt like a good idea actually turned

play18:24

out to have harmful consequences.

play18:26

And a typical example of such a situation in

play18:28

Swift would be when you try to use an advanced

play18:31

language feature in a context where it wasn't

play18:35

really meant to be used and it ends up doing

play18:38

more harm than good.

play18:39

Let's take the example of the keyword defer.

play18:43

Defer is a keyword that allows us to write

play18:46

code that will be executed only when the current

play18:49

scope exits.

play18:51

So typically when a function returns.

play18:54

Defer can actually be very useful when we are

play18:56

dealing with symmetrical operations.

play18:59

So think of things like opening and closing a file,

play19:02

starting and stopping a process,

play19:05

allocating and deallocating memory,

play19:07

that kind of thing.

play19:08

Because forgetting to make the symmetrical

play19:10

call would be a dangerous programming error,

play19:12

defer is actually very useful in this specific

play19:16

situation.

play19:17

However, it is very important to realize that

play19:20

defer works by breaking a very important property.

play19:25

Indeed,

play19:26

defer decouples the place where the code is

play19:28

written with the time when it is actually executed.

play19:32

This means that when we use defer,

play19:34

we can no longer assume that our code will

play19:38

be executed in the order in which it was written.

play19:41

When we are dealing with symmetrical calls,

play19:43

the upside of using defer makes this change

play19:46

definitely worth it.

play19:48

However, in other contexts,

play19:50

using defer could very easily turn out to be

play19:53

a bad practice because it is almost guaranteed

play19:56

to make our code more confusing without necessarily

play20:00

providing a significant improvement in exchange.

play20:04

That's all for the answer, now let's move on to recap.

play20:07

And for the recap, we can say that Swift is

play20:10

a very rich programming language with a lot

play20:12

of advanced features, but we must be careful

play20:15

when using them because their indiscriminate

play20:17

use can very quickly turn into bad practices.

play20:21

And this isn't even specific to Swift because

play20:24

software engineering as a whole is a lot about

play20:27

making the correct trade-offs.

play20:29

And so, as a general rule, whenever we want

play20:32

to use a fancy trick, we should always be thorough

play20:35

and carefully consider both the potential upside

play20:39

but also the potential downsides.

play20:42

So for this sixth question, we are back to

play20:45

a very technical topic because the question

play20:47

is what's the difference between weak and unowned?

play20:51

And because it's a very technical question,

play20:54

it's actually quite challenging to deliver

play20:56

an answer that will both be correct but that

play20:59

will also feel very structured and very clear.

play21:02

So let's see how our framework can help us here.

play21:05

With this kind of very technical question,

play21:07

the best way to contextualize is to just give

play21:10

very clear definition of what we are talking about.

play21:13

So here we can state that weak and unowned

play21:16

are two attributes that we can use to refer

play21:19

to an instance without taking a strong reference to it.

play21:23

And we typically use weak or unowned in situations

play21:27

where taking a strong reference to an instance

play21:29

would result in a retained cycle,

play21:31

for instance, inside a closure or when storing

play21:35

a delegate.

play21:36

That should be enough context.

play21:37

Now let's move on to the answer itself.

play21:39

To answer the question, we begin by giving

play21:42

precise definitions of weak and unowned.

play21:45

A weak reference is meant to be used when the

play21:48

other instance has a shorter lifetime.

play21:51

Because of this shorter lifetime,

play21:54

it's possible for the other instance to be

play21:56

deallocated by the time that we use the weak reference.

play22:00

To account for this possibility,

play22:02

the weak reference will be stored using an optional.

play22:06

This way, when the other instance has been deallocated,

play22:09

the weak reference will store nil.

play22:11

And on the other hand, an unowned reference

play22:14

is meant to be used when the other instance

play22:16

has an equal or longer lifetime.

play22:20

And because of this equal or longer lifetime,

play22:22

we assume that the other instance will never

play22:26

be deallocated when we use the unowned reference.

play22:29

Consequently,

play22:30

an unowned reference will be implicitly unwrapped.

play22:34

That's more than enough for the answer.

play22:35

Now let's tie everything together during the recap.

play22:39

We begin the recap by stating that both weak

play22:42

and unowned references allow us to refer to

play22:45

other instances without the risk of creating

play22:48

a retained cycle.

play22:49

However, we definitely want to be careful when

play22:52

using unowned references.

play22:54

Because if our assumption that the other instance

play22:57

will have an equal or longer lifetime turns

play23:00

out to be wrong,

play23:01

then accessing the unowned reference will result

play23:04

in a crash.

play23:05

So if we want to play it safe, it actually

play23:07

makes sense to only use weak references inside

play23:10

a codebase.

play23:11

It's worth noting that because a weak reference

play23:14

has to deal with the extra logic for the case

play23:18

where the other instance has been deallocated,

play23:21

it is a little less optimized than an unowned reference.

play23:25

However, unless you're writing some computation

play23:27

-intensive code that requires every possible

play23:31

optimization,

play23:32

there's a very good chance that you will never

play23:34

notice any difference in performance by using

play23:38

a weak reference over an unowned reference.

play23:41

The seventh question is when would you use

play23:43

the attribute at auto-closure?

play23:46

That's an interesting one because with such a question,

play23:49

the interviewer usually wants to evaluate two things.

play23:52

First, they want to see whether or not you

play23:55

know what the attribute at auto-closure is.

play23:58

And then they want to see if you have enough

play24:00

coding maturity to know when it's a good idea

play24:03

and when it's not a good idea to use such an attribute.

play24:07

So let's answer the question.

play24:09

By now, you should have a good idea of how

play24:11

to contextualize that kind of question.

play24:14

We contextualize it by giving a definition

play24:16

of the attribute in question.

play24:19

So here at auto-closure is an attribute that

play24:22

we can apply to a closure argument of a function.

play24:25

And when that attribute is applied, then the

play24:28

expression that we pass as an argument will

play24:30

be automatically wrapped inside a closure.

play24:33

This behavior might seem weird at first glance,

play24:36

but it actually serves an interesting purpose.

play24:40

Imagine that the expression you pass as an

play24:42

argument is potentially expensive to compute

play24:46

and that at the same time,

play24:48

the function you pass it to might not always

play24:51

need to use it.

play24:52

In that situation, it would make a lot of sense

play24:55

to have a mechanism that would allow the costly

play24:58

expression to be evaluated only when we need to use it.

play25:02

Wrapping the expression inside a closure would

play25:05

achieve that goal, but it would also degrade

play25:08

the readability of the call sites because we

play25:11

would need to add a pair of curly braces every time.

play25:14

And that's when the attribute at auto-closure

play25:17

becomes useful.

play25:18

This attribute will automatically wrap the

play25:21

expression inside a closure.

play25:23

This way, we get all the benefits without degrading

play25:26

the readability of the call sites.

play25:29

A typical example of when at auto-closure can

play25:32

be useful is if we want to implement a logging

play25:35

function because logs are not always enabled

play25:39

or all not logging levels are not always enabled

play25:42

and the data we log can be costly to evaluate.

play25:46

Think for instance if we are dumping a big

play25:49

object hierarchy.

play25:51

And now that we have explained when it would

play25:52

make sense to use at auto-closure,

play25:55

let's tie everything together by moving into the recap.

play25:58

We can start by saying that while at auto-closure

play26:02

can definitely help us optimize our code without

play26:06

degrading its readability,

play26:08

like with any sugar syntax, it's important

play26:11

to also consider its flip side.

play26:13

In the case of auto-closure,

play26:15

it's not obvious at the call site that the

play26:17

expression will be wrapped inside a closure

play26:20

and so will not be evaluated immediately if at all.

play26:25

And it's important to keep this in mind because

play26:27

it could lead to subtle bugs in potential edge

play26:31

cases, like for instance, if we pass as an

play26:34

argument an expression that also carries out

play26:37

a side effect.

play26:38

For this eighth question, we are back to a

play26:40

much more open topic with the question,

play26:43

what open source tools do you recommend using and why?

play26:47

With this kind of question, the contextuation

play26:49

is usually quite easy.

play26:51

We can just say that the iOS community has

play26:54

a very active open source scene, which produces

play26:57

a lot of useful tools to improve the quality

play27:00

of a project.

play27:01

And while it's near impossible to go over all

play27:04

the possible valuable tools, we can definitely

play27:07

go over the most common ones.

play27:09

We can begin by talking about SwiftLint and

play27:12

SwiftFormat, which are two linters that make

play27:15

it easier to enforce coding rules and coding

play27:18

style across our code base.

play27:20

Then we can talk about Fastlane, which is an

play27:23

extremely popular command line tool whose goal

play27:26

is to automate the shipping process of an iOS

play27:29

app from signing the code to uploading it to

play27:33

the app store.

play27:34

Then we can talk of SwiftGen, which is a great

play27:37

tool that will generate swift data structures

play27:40

so that we can manipulate the resources of

play27:43

our Xcode project in a type safe manner.

play27:46

And while we're on the topic of code generation

play27:48

tool, we can also mention Sorcery, which is

play27:51

a general purpose code generation tool that

play27:54

can be very useful to automate the writing

play27:57

of boilerplate code.

play27:59

Think, for instance, of implementing mocked

play28:02

implementation of protocols.

play28:04

And finally, we can mention that older projects

play28:07

are very likely to use an open source tool

play28:10

to manage their dependencies with tools like

play28:12

CocoaPods or Carthage.

play28:14

However, more recent projects are much more

play28:17

likely to use Apple's own Swift package manager

play28:20

for this job.

play28:22

And for the recap, we can say that there are

play28:24

a lot of useful open source tools out there

play28:27

that do a great job at providing the features

play28:30

that are still missing from Xcode.

play28:32

But of course, the goal here is definitely

play28:34

not to bloat our project by using every open

play28:38

source tool possible, but rather to carefully

play28:41

evaluate whether a tool is actually relevant

play28:44

to our current needs or not.

play28:46

We are getting close to the end.

play28:48

This is already question 9.

play28:50

And this is a very interesting question.

play28:53

How do you choose the minimum version for an iOS app?

play28:58

So let's answer it and start with a bit of context.

play29:01

Here, it can be useful to remind that all iOS

play29:04

apps must decide what is the lowest version

play29:08

of iOS that they support.

play29:11

And because new APIs are,

play29:13

unless exception,

play29:14

not backported to older versions of iOS,

play29:18

this choice has some strong technical consequences.

play29:22

For instance,

play29:23

SwiftUI isn't available before iOS 13 and,

play29:27

to be honest, only becomes really usable with iOS 14.

play29:32

The same way, Combine also isn't available

play29:35

before iOS 13.

play29:37

And so, because you might not be able to use

play29:40

important APIs,

play29:42

the choice of the lowest iOS version your app

play29:45

supports will have some major consequences

play29:48

on the technical choices you will be making

play29:51

as a developer.

play29:53

However, this very important choice cannot

play29:55

be only decided from the technical point of view.

play29:59

Because as developers, we would, of course,

play30:02

like to support only the most recent version.

play30:05

This way, we would get access to all the modern

play30:07

APIs and we would also save a ton of time testing

play30:11

our app on older versions of iOS.

play30:15

But doing so would also make our app unusable

play30:18

by a potentially large amount of iPhone users.

play30:22

And this is why this choice not only has some

play30:25

strong technical implications but also strong

play30:29

business implications.

play30:31

And there's no clear-cut answer here.

play30:33

It really depends on the kind of app that you

play30:36

are working on.

play30:37

For example,

play30:38

apps that deliver important services like banking

play30:42

or the administration will usually want to

play30:44

support a large number of iOS versions.

play30:48

And on the other hand,

play30:49

apps that deliver cutting-edge tech can assume

play30:52

that their users update their device more regularly

play30:55

and so can afford to support less iOS versions.

play30:59

That was definitely a long answer,

play31:02

so now it's time to move on to the recap.

play31:05

We can recap by stating that deciding the minimum

play31:08

version of iOS that an app will support is

play31:12

definitely not an easy choice and is often

play31:15

the result of a compromise between business

play31:18

and engineering.

play31:19

And one important thing to keep in mind if

play31:22

you're about to start a new app is to make

play31:25

sure that you decide its minimum iOS version

play31:28

based on when the app is actually expected to release,

play31:34

which could very easily be months or even a

play31:38

year away from now.

play31:39

So we've reached the last question of this

play31:42

training course.

play31:43

This one is all about architecture and design patterns.

play31:47

And the question is, can you explain what's

play31:50

dependency injection?

play31:51

As usual,

play31:52

let's answer the question using our framework.

play31:56

So we start by contextualizing the question.

play31:59

And we can contextualize by saying that it's

play32:02

a good software practice to partition our code

play32:06

in several components, each tasked with fulfilling

play32:10

a specific responsibility.

play32:12

But of course, the question that comes immediately

play32:15

after saying that is,

play32:17

how do we put all the pieces of the puzzle

play32:20

back together?

play32:21

A naive but working approach would be to make

play32:24

each component responsible for both creating

play32:27

and using its dependencies.

play32:30

And while this would work, it would also strongly

play32:33

decrease the modularity of our code base because

play32:36

it would then become impossible to swap one

play32:40

component for another.

play32:41

And to solve this issue, we need a way to decouple

play32:44

the creation of a dependency with its actual use.

play32:48

This process is exactly what's called dependency

play32:51

injection.

play32:52

And here's how it typically looks like.

play32:55

First, we make each dependency expose its API

play32:59

through a protocol.

play33:01

And then we update the initializer of our component,

play33:04

adding a new argument for each dependency and

play33:08

using the protocols as the types.

play33:10

This way, the actual creation of the dependencies

play33:13

can be handled in a separate part of our code,

play33:16

and our component can focus solely on using

play33:21

its dependencies.

play33:22

As a result, our code base has become much more modular.

play33:26

We can, for instance, inject mocked versions

play33:29

of our dependencies when we run our tests.

play33:33

For the recap, we can start by saying that

play33:35

dependency injection is a typical example of

play33:38

using a $5 word to describe a $0.50 concept

play33:43

because the name sounds super complex and intimidating,

play33:47

whereas the actual idea behind it is relatively

play33:51

simple and straightforward.

play33:53

It's also worth saying that this way of implementing

play33:56

dependency injection is not always possible

play33:59

because it requires you to have control over

play34:02

the initializer of your component.

play34:04

And we actually have a great example of this on iOS.

play34:08

You might have noticed that views defined in

play34:11

a storyboard don't have their subviews injected

play34:14

through their initializer, but rather through

play34:17

properties that are implicitly unwrapped, the

play34:20

properties annotated with at ib outlet.

play34:23

And the reason for this choice is because back

play34:25

in Objective-C, it wasn't really possible to

play34:28

create a dedicated initializer for each possible

play34:32

combination of dependencies.

play34:34

So that's it.

play34:35

You've reached the end of this training course.

play34:37

I hope you've enjoyed it, that you've learned

play34:39

new things, and that you now feel more confident

play34:42

to answer interview questions.

play34:43

If you want to apply what you've learned, I've

play34:46

put together this list of 10 new questions

play34:49

that you can try to answer using the framework

play34:52

we've just learned together.

play34:54

Once again, thank you for watching this training

play34:56

course, and see you next time!

Rate This

5.0 / 5 (0 votes)

Do you need a summary in English?