How to Answer iOS Interview Questions Like a Pro 👩🏽💻👨🏻💻 (free training course)
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
📱 iOS面试指南开篇
本视频由Revenue Cat赞助,原为付费课程现免费提供。视频介绍了一种简单框架,用于结构化、清晰、有影响力地回答iOS面试问题。通过10个示例展示如何应用这一框架回答典型的iOS技术面试问题,旨在帮助观众在iOS面试中有效运用此框架。
🔍 探索Optional
介绍了Optional在Swift中的概念,它是处理可能为nil值的类型安全方式。通过比较安全和不安全的处理Optional的方法,强调了优先选择安全方法以避免程序崩溃的重要性。同时,指出Optional是Swift标准库的一部分,通过enum实现,虽然我们可以查看其GitHub上的实现代码,但编译器内置的语法糖是不可自行实现的。
🔄 理解@escaping和@nonescaping闭包
讨论了闭包在Swift中的两种类型:@escaping和@nonescaping。解释了闭包如何捕获值以及这种捕获机制如何导致内存泄漏。通过区分这两种闭包类型,强调了如何正确使用闭包来避免内存泄漏,特别是在使用@escaping闭包时需要额外小心。
🛠️ 计算属性与方法的选择
探讨了何时应使用计算属性而不是方法的问题。强调了计算属性应遵守的语义规则,如无副作用、幂等性和恒定时间执行。如果代码不符合这些规则,则应选择使用方法。这段讨论旨在指导开发者如何根据代码的行为选择最合适的封装方式。
🔑 Swift关键字guard的用途
讲解了Swift中guard关键字的用途,特别是在改善代码可读性方面的价值。通过将错误路径早早地从主流程中分离出来,guard能够让正常路径的代码结构更为清晰。同时,讨论了使用guard时需要注意的事项,如避免在条件中使用否定形式以减少读者的理解负担。
📦 探讨defer的正确使用
分析了Swift中defer关键字的使用场景,强调了它在处理对称操作时的价值,如打开/关闭文件或启动/停止进程。同时指出,不恰当的使用defer可能会导致代码更加混乱,不建议在不适合的场合使用defer,以免引入不必要的复杂性。
🔗 弱引用与无主引用的区别
详细解释了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
💡面试问题
💡上下文化
💡解答问题
💡总结答案
💡可选类型
💡闭包
💡计算属性
💡guard语句
💡弱引用
💡最小支持版本
Highlights
提出了一个简单的三步框架来帮助回答面试题目
第一步是概括问题,澄清任何假设
第二步是提供答案,解释或定义
最后一步是总结答案的要点
Optional是Swift处理潜在空值的类型安全机制
非逃逸闭包不会造成内存泄漏
计算属性与方法在技术上非常相似
guard关键字可以让代码更具可读性
当语法糖的要求没有完全满足时最好不要使用它
尽管defer很有用,但在其他上下文中使用它可能是一个坏习惯
weak引用处理其他实例被回收的情况
at自动闭包允许延迟评估参数
开源工具可以提高项目质量
最小iOS版本的选择需要平衡商业和工程之间的权衡
依赖注入通过解耦来提高代码的模块性
Transcripts
Welcome to this video where you'll learn how
to give great answers to technical iOS interview
questions.
The content you're about to see used to be
a paid training course, but thanks to the generous
sponsorship of Revenue Cat, it is now available
for free on YouTube.
So I have a big thank you for them, and if
you're curious to see how they can help you
integrate in-app purchases into your iOS app,
you have a link to learn more in the description.
That's all I wanted to say, enjoy the video.
Hey, welcome to this training course to learn
how to answer iOS interview questions like a pro.
So what's the plan for this course?
It's pretty straightforward.
First, I'm going to show you a simple framework
that you can apply to answer interview questions
in a way that feels structured,
clear, and impactful.
And then I will show you through 10 different
examples how to apply this framework to answer
typical questions that you could expect in
an iOS technical interview.
And my goal is that by the end of this course,
you will be able to efficiently apply this
framework the next time you're taking an iOS interview.
Answering an interview question can definitely
be stressful.
Someone is trying to evaluate you, you're trying
to get a job, so the stakes are definitely high.
And in that situation,
it's very easy to stumble,
be a bit confused, and basically deliver your
answer in a way that feels subpar.
To try and avoid this, I want to show you a
very simple framework in three steps that will
help you deliver your answer in a way that feels clear,
structured,
and impactful.
So let's see what are these three steps.
The first step is to start by contextualizing
your answer.
What do I mean by contextualize?
I mean that you should reformulate the question
and explicitly state any assumptions you're making.
You can see this as an introduction for your answer.
It's a way to nicely ease into the actual answer
and also give a bit of time to the interviewer
to fully switch into listening mode.
But this is also a great way for the interviewer
to correct you if for some reason you've made
a wrong assumption.
This way you don't waste time starting to deliver
an answer that's going into the wrong direction.
So once you're done contextualizing, it's time
to move on to the next step, which is to deliver
the actual answer.
Here it's relatively straightforward, you just
answer the question, meaning that you give
either the explanation,
the definition,
the best practice that the interviewer is expecting
to hear.
But after you're done answering the question,
it's not over, there is still one last step,
which is to recap your answer.
The goal here is to sum up the key points of
your answer.
This shouldn't take too long, just a few sentences,
but doing so will really help make your answer
feel more structured.
And if you have more knowledge on the topic
or on adjacent or related topics, this is also
a great time to mention it.
Don't forget that the goal of the interviewer
is to evaluate your skill level in general,
and the questions are just a proxy to achieve that goal.
And so any extra information you can share
that will help convey this feeling that you
do know your stuff will only have a positive impact.
So these are the three steps of this simple framework,
contextualize, answer,
and finally recap.
Now it's time to see this framework in action
to see how we can apply this framework to an
actual iOS interview question.
So let's apply our framework to answer a first
interview question.
And that question is,
what's an optional?
Back when I was interviewing candidates for
junior to mid-level positions, I would actually
always start the interview by asking this question
because I feel it's a nice simple question
and it's a great way to get the interview started.
So let's use our framework to answer this question.
The first step is to contextualize the question.
Here a great way to contextualize is to remind
the interviewer that one of the goals of Swift
is to be a safe programming language.
This means that unlike in other languages,
variable by default cannot store a null value.
This means that if you have a variable that
for instance is typed with int,
then the compiler will expect that variable
to hold an actual int and it won't be possible
to set a null value to the variable.
But of course there are situations where it
makes sense for a variable to actually hold
a null value and so Swift does offer a mechanism
to make a variable nullable and we opt in to
this mechanism by making the type of the variable
be an optional.
That's more than enough for the contextualization.
Now let's move on to the actual answer.
We can start by stating that to make a variable
be an optional,
we add a question mark to the type of the variable
which is nothing more than a shortcut for using
the generic type optional of t.
And by making a variable optional,
the compiler will actually make sure that we
explicitly deal with the potential nil value.
This is the big advantage of optional.
It offers a type safe approach to dealing with
values that can potentially be nil.
And we have basically two ways to deal with
the potential nil value.
First we can use the safe approach which is
by using syntaxes like optional chaining to
call a method on an optional or conditional binding,
the famous if let syntax.
And these approaches allow us to deal with
the optional in a way that is safe, meaning
that if the value is actually nil our code
won't crash but we will need to write a bit
more code to deal with the potential nil case.
And then we have unsafe approaches like for
instance force unwrapping.
These approaches will require a bit less syntax
because we don't need to deal with the potential
nil value but they have one big drawback which
is that if the value turns out to be nil then
your code will crash.
So of course most of the time we do want to
use the safe way of handling an optional because
we don't want our code to crash when there
is actually a nil value.
And unsafe approaches should really be used
with a lot of caution and should be reserved
to the rare cases where we are extremely sure
that the value will never be nil and we should
never lose sight of the fact that if for some
reason, for some unforeseen reason, the value
turns out to be nil then our program will crash.
So that's what a potential answer to this question
looks like.
Now let's move on to the last part of our framework,
the recap.
Here the recap is pretty straightforward.
We can just state that optional is Swift's
type safe mechanism to deal with potentially nil values.
And don't forget that the recap is also a great
time to share any extra knowledge that you
have on the topic.
For instance, here you can share the fact that
optional is actually implemented in Swift itself.
It's part of the standard library and you can
go see its code on GitHub.
And optional is actually implemented using
an enum with two cases,
none and then some with an associated value
to store the actual value.
However, while this means that we could basically
implement our own version of optional,
we couldn't implement everything because most
of the sugar syntax that makes optional quite
easy to use in Swift is itself implemented
in the compiler.
So that's all for this first question.
Now let's move on to the second one.
So for this second question, I picked a topic
that is slightly more complex and it's the
difference between at escaping and non-at escaping
closures.
So let's see how we can apply our framework
to this new question.
First, we start by once again contextualizing
the question.
Here a great way to contextualize is to give
a simple definition of what a closure is.
So something along these lines, a closure is
a piece of code that we write in one place,
but that is meant to be executed at a later time.
And then we move on to the fact that because
they will be executed later,
closures need to make sure that the values
they interact with will still be in memory
by the time they are executed.
And as we know, this ability of closures to
capture strong references is a double-edged
sword because it also opens up the way to memory
leaks through retained cycles.
I think that's good enough for the contextualization.
Now we can move on to delivering the actual answer.
So first, we can start by stating that not
all closures are meant to be used the same way.
We can really split closures into two different kinds.
First, we have the closures that are not meant
to have a lifetime that exceeds the one of
the function that they are passed to.
A typical example of such closures are the
ones you pass to functions like map or filter.
These closures are meant to be called inside
the body of the function that you pass them
to, but once that function has returned, they
no longer serve any purpose.
And these are the closures that we call non
-escaping closures.
The name reflects the fact that these closures
don't escape the scope of the function they've
been passed to.
And because their lifetime is limited to that
of the function they've been passed to, they
don't pose a threat regarding memory safety
because they cannot create a memory leak since
their lifetime is limited.
This means that inside a non-escaping closure,
we don't actually need to worry about capturing
a strong reference because it's just not possible
for such a closure to lead to a retained cycle.
And in Swift, closures are actually non-escaping
by default.
However, some closures do have the legitimate
need to outlive the function that they've been
passed to.
Think for instance of the closure that handles
the result of a network call.
Such a closure will be executed after that
the function that started the network call has returned.
Such closures are called escaping closures
because they escape the lifetime of the function
they've been passed to.
And in Swift, they must be explicitly annotated
with the attribute at escaping.
And inside such closures, we must be very careful
whenever we capture a strong reference to an
instance because these are the kind of closures
that have the potential for creating a retained cycle.
However, Swift does provide us with the tools
to capture references in safe manners inside
such closures by using the closures capture list.
A typical example of this is when we capture
a weak reference to self inside a closure that
will be directly or indirectly also be stored in self.
I think it's more than enough for the answer.
Now we can move on to the recap.
So for the recap, we can just state that closures
are known to be one of the top cause of memory
leaks in iOS.
But not all closures have the potential to
create such memory leaks.
Only closures that escape the lifetime of the
function they're passed to can potentially be harmful.
And that's why Swift forces us to explicitly
annotate such closures with at escaping.
But you want to be careful because there does
exist some rare instances where at escaping
will actually be implicit.
An example of that is when you pass an optional
closure as an argument.
In that situation, the at escaping attribute
won't be explicitly written,
but the closure will still behave like an escaping
closure.
That's all for this second question.
Let's move to the next one.
So with this new question, we're going into
the topic of good practices and code readability.
The question is when would you use a computed
property versus a method?
Once again, let's apply our framework to answer
this question.
We begin by contextualizing our answer.
So here, a great way to contextualize the answer
is to just reformulate the question.
So we can state that in Swift, when we want
to encapsulate a piece of code that produces a value,
we basically have two potential choices.
We can put that piece of code either inside
a computed property or inside a method.
And the interesting thing here is that from
a technical point of view, computed properties
and methods are actually quite similar.
When you think about it, a computed property
is actually nothing else than a method with
no argument and a simplified syntax to call it.
And because of that, the choice between the
two options cannot really be decided from the
technical point of view, but rather should
be decided from a semantics point of view,
meaning we should consider what the code actually does.
Because in Swift, properties are expected to
have some specific semantics.
They shouldn't have side effects, meaning that
calling a computed property should not produce
a side effect in another part of our code.
They should also be idempotent, which is a
mathematical word to say that if we call the
properties several times successively in a
row, it should always return the same value.
So for instance, a property shouldn't be used
to implement a random value because getting
a random value is definitely not idempotent.
The entire idea is to get a different value every time.
And finally, properties should execute in constant time.
So in terms of time complexity,
they should be O of 1, meaning that if for
instance you call a computed property on a collection,
the time the property will take to execute
should not depend on the size of the If all
these properties are met, then it's absolutely
okay to encapsulate our code to produce a value
using a computed property.
However,
if not all of them are met, then it's better
to use a method because the code we have doesn't
behave like we expect a property to behave.
Because if we were to still encapsulate the
code inside a computed property, it's basically
guaranteed to generate trouble at some point
when another person will use the property and
will expect it to behave in a way that it doesn't.
So that was a big answer.
Now let's move on to the recap.
So for this recap, we can conclude by stating
that syntax sugar, like compute properties,
can be really useful to write shorter code.
However,
we must be really careful to only use syntax
sugar when it actually makes sense.
And it's usually a bad idea to try to use a
syntax sugar when not 100% of its requirements
have been met because it's basically guaranteed
to lead to trouble at some point.
So for this fourth question, I chose something
that is still on the topic of code readability
by asking what is the purpose of the Swift
keyword guard?
Once again, let's apply our framework to answer
this question.
So how can you contextualize this question?
You can start by saying that when we implement
a feature in an iOS app, we usually split our
code into two parts.
First, we have what we call the happy path,
meaning the code that implements our feature
assuming that everything is working correctly.
And then we also have the error paths, which
deal with all the potential technical and business
errors that can happen.
So think of things like having no network connection,
having an expired authentication token,
using an invalid promo code, that kind of things.
And ideally, we would like the structure of
our code to be such that the happy path is
the main flow of the code and the error paths
are branching out of this main flow.
However, if we implement our code using if
statements to make sure that all the conditions
are met, it's actually the opposite that will
be happening.
What will happen is that each new error path
and so each new if statement will cause the
happy path to become more and more deeply nested.
And as we can imagine, this is when the keyword
guard comes into play.
Guard will allow us to reverse the structure
of the code by implementing an early return
from the function whenever a condition has not been met.
And so by using guard, we will be able to refactor
our code into a much more readable structure
with the happy path at the center, like we
initially wanted.
So that was the answer.
Now let's finish with the recap.
And for the recap, we can simply state that
guard is the typical example of when seemingly
simple sugar syntax actually has the potential
of dramatically improving the readability of our code.
However, like with all sugar syntax, we want
to be careful because they always come with a flip side.
In the case of guard, we want to be careful
when using a negation inside the condition
because then the statement ends up being a
double negation, which can be hard to read
and potentially confusing.
For this next question, I picked a much more
open topic by asking to give an example of
a bad practice in Swift.
So once again, let's answer the question using
our framework and start by contextualizing the question.
Here we can contextualize by stating that a
bad practice is usually a situation where what
initially felt like a good idea actually turned
out to have harmful consequences.
And a typical example of such a situation in
Swift would be when you try to use an advanced
language feature in a context where it wasn't
really meant to be used and it ends up doing
more harm than good.
Let's take the example of the keyword defer.
Defer is a keyword that allows us to write
code that will be executed only when the current
scope exits.
So typically when a function returns.
Defer can actually be very useful when we are
dealing with symmetrical operations.
So think of things like opening and closing a file,
starting and stopping a process,
allocating and deallocating memory,
that kind of thing.
Because forgetting to make the symmetrical
call would be a dangerous programming error,
defer is actually very useful in this specific
situation.
However, it is very important to realize that
defer works by breaking a very important property.
Indeed,
defer decouples the place where the code is
written with the time when it is actually executed.
This means that when we use defer,
we can no longer assume that our code will
be executed in the order in which it was written.
When we are dealing with symmetrical calls,
the upside of using defer makes this change
definitely worth it.
However, in other contexts,
using defer could very easily turn out to be
a bad practice because it is almost guaranteed
to make our code more confusing without necessarily
providing a significant improvement in exchange.
That's all for the answer, now let's move on to recap.
And for the recap, we can say that Swift is
a very rich programming language with a lot
of advanced features, but we must be careful
when using them because their indiscriminate
use can very quickly turn into bad practices.
And this isn't even specific to Swift because
software engineering as a whole is a lot about
making the correct trade-offs.
And so, as a general rule, whenever we want
to use a fancy trick, we should always be thorough
and carefully consider both the potential upside
but also the potential downsides.
So for this sixth question, we are back to
a very technical topic because the question
is what's the difference between weak and unowned?
And because it's a very technical question,
it's actually quite challenging to deliver
an answer that will both be correct but that
will also feel very structured and very clear.
So let's see how our framework can help us here.
With this kind of very technical question,
the best way to contextualize is to just give
very clear definition of what we are talking about.
So here we can state that weak and unowned
are two attributes that we can use to refer
to an instance without taking a strong reference to it.
And we typically use weak or unowned in situations
where taking a strong reference to an instance
would result in a retained cycle,
for instance, inside a closure or when storing
a delegate.
That should be enough context.
Now let's move on to the answer itself.
To answer the question, we begin by giving
precise definitions of weak and unowned.
A weak reference is meant to be used when the
other instance has a shorter lifetime.
Because of this shorter lifetime,
it's possible for the other instance to be
deallocated by the time that we use the weak reference.
To account for this possibility,
the weak reference will be stored using an optional.
This way, when the other instance has been deallocated,
the weak reference will store nil.
And on the other hand, an unowned reference
is meant to be used when the other instance
has an equal or longer lifetime.
And because of this equal or longer lifetime,
we assume that the other instance will never
be deallocated when we use the unowned reference.
Consequently,
an unowned reference will be implicitly unwrapped.
That's more than enough for the answer.
Now let's tie everything together during the recap.
We begin the recap by stating that both weak
and unowned references allow us to refer to
other instances without the risk of creating
a retained cycle.
However, we definitely want to be careful when
using unowned references.
Because if our assumption that the other instance
will have an equal or longer lifetime turns
out to be wrong,
then accessing the unowned reference will result
in a crash.
So if we want to play it safe, it actually
makes sense to only use weak references inside
a codebase.
It's worth noting that because a weak reference
has to deal with the extra logic for the case
where the other instance has been deallocated,
it is a little less optimized than an unowned reference.
However, unless you're writing some computation
-intensive code that requires every possible
optimization,
there's a very good chance that you will never
notice any difference in performance by using
a weak reference over an unowned reference.
The seventh question is when would you use
the attribute at auto-closure?
That's an interesting one because with such a question,
the interviewer usually wants to evaluate two things.
First, they want to see whether or not you
know what the attribute at auto-closure is.
And then they want to see if you have enough
coding maturity to know when it's a good idea
and when it's not a good idea to use such an attribute.
So let's answer the question.
By now, you should have a good idea of how
to contextualize that kind of question.
We contextualize it by giving a definition
of the attribute in question.
So here at auto-closure is an attribute that
we can apply to a closure argument of a function.
And when that attribute is applied, then the
expression that we pass as an argument will
be automatically wrapped inside a closure.
This behavior might seem weird at first glance,
but it actually serves an interesting purpose.
Imagine that the expression you pass as an
argument is potentially expensive to compute
and that at the same time,
the function you pass it to might not always
need to use it.
In that situation, it would make a lot of sense
to have a mechanism that would allow the costly
expression to be evaluated only when we need to use it.
Wrapping the expression inside a closure would
achieve that goal, but it would also degrade
the readability of the call sites because we
would need to add a pair of curly braces every time.
And that's when the attribute at auto-closure
becomes useful.
This attribute will automatically wrap the
expression inside a closure.
This way, we get all the benefits without degrading
the readability of the call sites.
A typical example of when at auto-closure can
be useful is if we want to implement a logging
function because logs are not always enabled
or all not logging levels are not always enabled
and the data we log can be costly to evaluate.
Think for instance if we are dumping a big
object hierarchy.
And now that we have explained when it would
make sense to use at auto-closure,
let's tie everything together by moving into the recap.
We can start by saying that while at auto-closure
can definitely help us optimize our code without
degrading its readability,
like with any sugar syntax, it's important
to also consider its flip side.
In the case of auto-closure,
it's not obvious at the call site that the
expression will be wrapped inside a closure
and so will not be evaluated immediately if at all.
And it's important to keep this in mind because
it could lead to subtle bugs in potential edge
cases, like for instance, if we pass as an
argument an expression that also carries out
a side effect.
For this eighth question, we are back to a
much more open topic with the question,
what open source tools do you recommend using and why?
With this kind of question, the contextuation
is usually quite easy.
We can just say that the iOS community has
a very active open source scene, which produces
a lot of useful tools to improve the quality
of a project.
And while it's near impossible to go over all
the possible valuable tools, we can definitely
go over the most common ones.
We can begin by talking about SwiftLint and
SwiftFormat, which are two linters that make
it easier to enforce coding rules and coding
style across our code base.
Then we can talk about Fastlane, which is an
extremely popular command line tool whose goal
is to automate the shipping process of an iOS
app from signing the code to uploading it to
the app store.
Then we can talk of SwiftGen, which is a great
tool that will generate swift data structures
so that we can manipulate the resources of
our Xcode project in a type safe manner.
And while we're on the topic of code generation
tool, we can also mention Sorcery, which is
a general purpose code generation tool that
can be very useful to automate the writing
of boilerplate code.
Think, for instance, of implementing mocked
implementation of protocols.
And finally, we can mention that older projects
are very likely to use an open source tool
to manage their dependencies with tools like
CocoaPods or Carthage.
However, more recent projects are much more
likely to use Apple's own Swift package manager
for this job.
And for the recap, we can say that there are
a lot of useful open source tools out there
that do a great job at providing the features
that are still missing from Xcode.
But of course, the goal here is definitely
not to bloat our project by using every open
source tool possible, but rather to carefully
evaluate whether a tool is actually relevant
to our current needs or not.
We are getting close to the end.
This is already question 9.
And this is a very interesting question.
How do you choose the minimum version for an iOS app?
So let's answer it and start with a bit of context.
Here, it can be useful to remind that all iOS
apps must decide what is the lowest version
of iOS that they support.
And because new APIs are,
unless exception,
not backported to older versions of iOS,
this choice has some strong technical consequences.
For instance,
SwiftUI isn't available before iOS 13 and,
to be honest, only becomes really usable with iOS 14.
The same way, Combine also isn't available
before iOS 13.
And so, because you might not be able to use
important APIs,
the choice of the lowest iOS version your app
supports will have some major consequences
on the technical choices you will be making
as a developer.
However, this very important choice cannot
be only decided from the technical point of view.
Because as developers, we would, of course,
like to support only the most recent version.
This way, we would get access to all the modern
APIs and we would also save a ton of time testing
our app on older versions of iOS.
But doing so would also make our app unusable
by a potentially large amount of iPhone users.
And this is why this choice not only has some
strong technical implications but also strong
business implications.
And there's no clear-cut answer here.
It really depends on the kind of app that you
are working on.
For example,
apps that deliver important services like banking
or the administration will usually want to
support a large number of iOS versions.
And on the other hand,
apps that deliver cutting-edge tech can assume
that their users update their device more regularly
and so can afford to support less iOS versions.
That was definitely a long answer,
so now it's time to move on to the recap.
We can recap by stating that deciding the minimum
version of iOS that an app will support is
definitely not an easy choice and is often
the result of a compromise between business
and engineering.
And one important thing to keep in mind if
you're about to start a new app is to make
sure that you decide its minimum iOS version
based on when the app is actually expected to release,
which could very easily be months or even a
year away from now.
So we've reached the last question of this
training course.
This one is all about architecture and design patterns.
And the question is, can you explain what's
dependency injection?
As usual,
let's answer the question using our framework.
So we start by contextualizing the question.
And we can contextualize by saying that it's
a good software practice to partition our code
in several components, each tasked with fulfilling
a specific responsibility.
But of course, the question that comes immediately
after saying that is,
how do we put all the pieces of the puzzle
back together?
A naive but working approach would be to make
each component responsible for both creating
and using its dependencies.
And while this would work, it would also strongly
decrease the modularity of our code base because
it would then become impossible to swap one
component for another.
And to solve this issue, we need a way to decouple
the creation of a dependency with its actual use.
This process is exactly what's called dependency
injection.
And here's how it typically looks like.
First, we make each dependency expose its API
through a protocol.
And then we update the initializer of our component,
adding a new argument for each dependency and
using the protocols as the types.
This way, the actual creation of the dependencies
can be handled in a separate part of our code,
and our component can focus solely on using
its dependencies.
As a result, our code base has become much more modular.
We can, for instance, inject mocked versions
of our dependencies when we run our tests.
For the recap, we can start by saying that
dependency injection is a typical example of
using a $5 word to describe a $0.50 concept
because the name sounds super complex and intimidating,
whereas the actual idea behind it is relatively
simple and straightforward.
It's also worth saying that this way of implementing
dependency injection is not always possible
because it requires you to have control over
the initializer of your component.
And we actually have a great example of this on iOS.
You might have noticed that views defined in
a storyboard don't have their subviews injected
through their initializer, but rather through
properties that are implicitly unwrapped, the
properties annotated with at ib outlet.
And the reason for this choice is because back
in Objective-C, it wasn't really possible to
create a dedicated initializer for each possible
combination of dependencies.
So that's it.
You've reached the end of this training course.
I hope you've enjoyed it, that you've learned
new things, and that you now feel more confident
to answer interview questions.
If you want to apply what you've learned, I've
put together this list of 10 new questions
that you can try to answer using the framework
we've just learned together.
Once again, thank you for watching this training
course, and see you next time!
Voir Plus de Vidéos Connexes
5.0 / 5 (0 votes)