Как стать автором
Обновить

Подготовка к собеседованию на iOS разработчика (актуально на начало 2023 года)

Время на прочтение14 мин
Количество просмотров11K

Небольшая предыстория

Доброго времени суток. Я iOS-разработчик (хлопки-хлопки).

Так получилось, что работал я iOS-разработчиком на одном легаси-проекте в компании-вендоре. В октябре прошлого года появилось желание сменить текущий проект, чтобы перестать волноваться о легаси и начать развиваться.

Поговорив с начальством, мне назначили встречу с лидом iOS, чтобы подготовить меня к предстоящим собеседованиям. Однако, после нескольких вопросов от лида вердикт был, что не подхожу на текущие проекты. На вопрос, какие, собственно, критерии оценивания я получил ответ:

"тебе никогда никто не расскажет? в нашем деле все сам ?"

Сам так сам. Я начал читать, смотреть курсы, видеоуроки и активно подаваться в разные компании как в России, так и за рубежом, чтобы выяснить, какие, собственно говоря, критерии прохождения. Постепенно раз за разом сформировался список наиболее часто встречающихся вопросов на собеседованиях iOS-разработчика, который я решил опубликовать здесь.

Технические вопросы

Т.к. собеседования были в основном на английском языке, то с вашего позволения оставлю тут и вопросы и ответы на английском. Поехали.

1. What frameworks you used in your iOS projects?

You can specify frameworks like: UIKit, SwiftUI, Combine, AVFramework, PushNotification, CallKit, GCD, Core Bluetooth, etc.

2. How the Optional is implemented in Swift?

enum Optional<Wrapped>
{
    case none
    case some(Wrapped)
}

3. Is there a difference between .none and nil?

No

4. Ways to unwrap optional variables

var a:Int?
//1
a!
//2
if let s = a {
    print(s)
}
//3
if let a {
    print(a)
}
//4
guard let s = a else {
    print(s)
    return
}
//5
a ?? 0 //5

5. What's difference between reference and value types? What reference and value types you know?

Value types: structs, enums, arrays, dictionaries, strings

Reference types: classes, closures, NS types (NSString, for example) (because they are classes)

Two differences between value and reference types:

  1. Value types are stored in Stack and reference types are stored in Heap.

  2. If you assign one object to another for a reference type, you just copy the reference, not value:

// Reference type example
class C { var data: Int = -1 }
var x = C()
var y = x						// x is copied to y
x.data = 42						// changes the instance referred to by x (and y)
println("\(x.data), \(y.data)")	// prints "42, 42"

For value types you will copy the value of the variable.

6. What's difference between Class and Structure?

  • Structures are value types.

  • Classes are reference types.

  • Structures don’t support inheritance.

  • classes support inheritance.

  • Structures don’t support de-initializers. ( deinit )

  • Classes support de-initializers.

  • Structures don’t follow Reference Counting (look at the question 19 about ARC).

  • Classes follow Reference Counting.

  • Mutating Keyword is needed to modify the property values in Structure’s instance methods.

  • No need of mutating keyword to modify the class variable’s value.

7. Do you know what copy-on-write means?

If you assign one array to another (not only array, there are other objects), then the second object will refer to the first array address until the second array is not changed.

func addressOf(_ o: UnsafeRawPointer) -> String {
    let addr = unsafeBitCast(o, to: Int.self)
    return String(format: "%p", addr)
}
//our array
var array = [1, 2, 3, 4, 5]
addressOf(array) // 0x600002e30ac0
//here we assign one array to another
var array2 = array
//look - the address of the second array is the same
addressOf(array2) // 0x600002e30ac0
//here we change the second array 
array2.append(6)
//look again - address has changed
addressOf(array2) // 0x6000026119f0

8. Do you know what are SOLID principles?

SOLID is abbreviation.

S – single responsibility principle.

It’s when a class has just one purpose. A class shouldn’t contain functions that could be moved to other classes

O – open/closed principle (OCP)

A class should be opened for extension, but closed for changes.

Open closed principle allows to avoid this kind of code:

protocol SomeProtocol {
}

class A:SomeProtocol {
    func printClassAName() {
        print("I'm A")
    }
}

class B:SomeProtocol {
    func printClassBName() {
        print("I'm B")
    }
}

class Caller {
    func printClassName(obj:SomeProtocol){
      ////TO AVOID THIS KIND OF CODE!!!!!
        if let unwrappeObj = obj as? A {
            obj.printClassAName()
        }
        else if let unwrappeObj = obj as? B {
            obj.printClassBName()
        }
    }
}

It should be changed like that to avoid changes in Caller class in the future:

protocol SomeProtocol {
    func printClassName()
}

class A:SomeProtocol {
    func printClassName() {
        print("I'm A")
    }
}

class B:SomeProtocol{
    func printClassName() {
        print("I'm B")
    }
}

class Caller {
    func doSomething(obj:SomeProtocol){
        print(obj.printClassName())
    }
}

This principle is similar with D (dependency inversion principle), but OCP is more general. The DIP is an extension of the OCP.

L – Liskov principle.

In simple words this principle says that you need to have a possibility to use a parent and a child classes without any difference.

class A: SomeProtocol {
}

class B: SomeProtocol {
}

let a = A()
let b = B()
var a:[SomeProtocol] = [a, b]

I – interface segregation.

Classes SHOULDN'T implement protocol methods they don’t use. If we noticed this situation, just move these methods to a separate protocol.

D – dependency inversion.

It means that a class shouldn’t depend on low-level modules – they both should depend on an Abstraction

For example:

class FileSystemManager {
  func save(string: String) {
    // Open a file
    // Save the string in this file
    // Close the file
  }
}

class Handler {
    let fileManager = FilesystemManager()
    func handle(string: String) {
        fileManager.save(string: string)
    }
}

If in the future we’ll need to add other methods for saving data (data base, for example), we should inherit both FilesystemManager and this new data base interactor class from some Storage protocol and use it instead of FilesystemManager and other possible data saving ways:

class FileSystemManager:Storage {
    func save(string: String) {
        // Open a file
        // Save the string in this file
        // Close the file
    }
}

class DataBaseManager:Storage {
    func save(string: String) {
        // Open DB
        // Save the data
        // Close DB
    }
}

class Handler {
    let storage:Storage
    func handle(string: String) {
        storage.save(string: string)
    }
}

This principle is similar with OCP is more general. The DIP is an extension of the OCP.

Difference is that OCP is for similar functions, but DIP deals with the same input data

9. What is Singleton?

The main point of Singleton is to ensure that we initialized something only once and this "something" should be available from everywhere. For example, UIApplication.shared

P.S.: ServiceLocator – is a singleton with an array of some services

10. How are you doing your code reviews?

The best practice said that the code review should depend on CI/CD tests, a style guide, SOLID, and some linter (a syntax checker)

11. Application lifecycle

Use this:

Taken from Apple documentation
Taken from Apple documentation

12. ViewController lifecycle

  • ViewDidLoad - Called when you create the class and load from xib. Great for initial setup and one-time-only work.

  • ViewWillAppear - Called right before your view appears, good for hiding/showing fields or any operations that you want to happen every time before the view is visible. Because you might be going back and forth between views, this will be called every time your view is about to appear on the screen.

  • ViewDidAppear - Called after the view appears - great place to start an animations or the loading of external data from an API.

  • ViewWillDisappear/DidDisappear - Same idea as ViewWillAppear/ViewDidAppear.

  • ViewDidUnload/ViewDidDispose - In Objective-C, this is where you do your clean-up and release of stuff, but this is handled automatically so not much you really need to do here

P.S.: Can you say what ViewController lifecycle methods are calling when you started to segue from a view (A) to another view (B), but haven't finish it?

13. What architecture patterns you used?

Better to mention MVC, MVVM, VIPER, Clean Architecture. I recommend to implement test samples for each of these patterns.

14. What is VIPER?

VIPER – is an architecture pattern with these parts:

  • R – router – an entry point

  • E – entity – model (like in MVC, for example)

  • P – presenter – holds the reference to interactor, to a router and to a view

  • Presenter uses data, received using fetching data functions from Interactor to update View.

  • V – view. But with additional protocol with updating functions to call. For example, if we want to show an alert in a view, then we should ask the presenter to do that

  • I – Interactor handles business logic and data retrieval

15. What is Clean Architecture?

I used this scheme

Taken from here: https://medium.com/@info.vikaasyadav/flutter-clean-architecture-with-riverpod-7807e54228c4
Taken from here: https://medium.com/@info.vikaasyadav/flutter-clean-architecture-with-riverpod-7807e54228c4

16. What is MVVM?

MVVM - Model View ViewModel

  • Model - is the data layer.

  • View - is a view.

  • ViewModel - contains presentation logic (process data from Model to View, reacts on actions from the View and transfers these reactions to Model).

In the code tree there should be three different directories: Models, Views, ViewModels. Each of your classes should be represented separately there.

Sometimes it's wise to create an additional directory called Services, in which you can put your business logic.

P.S.: What is MVVM-C?

17. Who is an owner of data in MVVM?

Model is the data itself.

18. MVC and MVVM differences:

The main difference between MVC and MVVM is the role of the controller and view model. In MVC, the Controller handles user input and updates the Model and View. In MVVM the ViewModel handles user input and updates the Model and View, and the view is responsible for displaying the data.

19. What NS prefix means in some Swift and Objective-C classes?

It means next step (the name of one company)

20. How memory management works in iOS? (Automatic reference counter (ARC))

In this question it's better to tell about ARC and retain cycles.

Short explanation: ARC automatically keeps track of the number of references to an object, and when that number reaches zero, it deallocates the object. Counter decreases after an object releases. Any object deallocates after the counter is 0.

If two objects have a strong reference to each other – retain cycle. Use weak or unowned to avoid that.

(weak variables are presented as optional if to take a look on its type) (unowned variables are presented as usual (they can’t be nil))

My advise is to watch this video from Apple: 

https://developer.apple.com/videos/play/wwdc2021/10216/

21. What is map, flatMap, compatMap, reduce. Difference between map, flatMap, compatMap

Mathematically:

map:

var arr:[Int] = [1, 2, 3]
    arr = arr.map {
    return $0+1
}

flatMap (is equivalent to Array(s.map(transformation).joined())):

var arr:[Int] = [[1,2,3],[4,5,6]]
    arr = arr.flatMap {
    return $0
}

compatMap - same as map, but filters nil values

In Combine

map is used to transform each value emitted by a publisher using a provided closure. The closure takes in a value emitted by the publisher and returns a new value.

compatMap is similar to map, but it also filters out any values that are nil before emitting the new values

flatMap returns a new publisher with an emitted value as a parameter:

var imagesListSubject = PassthroughSubject<String, Error>()
                          imagesListSubject
                          .removeDuplicates()
                          .flatMap { [unowned self] day in
                            self.networkService.fetchDayImagesList(day: day)
                          }

22. How to make a multilevel dismiss in SwiftUI? (to dismiss multiple level navigation)

  1. You can use @EnvironmentObject, because it's available in all nested views

  2. You can transfer @Binding variable from the root view to new navigation levels and if you need, just toggle this variable

  3. Use an architecture pattern in which you can just set a current view. Like VIPER, REDUX and Composable architecture

23. What is a View protocol in SwiftUI?

In SwiftUI, the View protocol is the fundamental building block of layout and user interface. But without some content it can't exist

24. Why Views are structures in SwiftUI?

  • structs are simpler to work with and faster than classes

  • it takes less memory (it takes only what was set, without multilevel inheritance)

  • views that don’t mutate over time

25. Is there a way to use UIKit elements in SwiftUI?

Yes. You should create a class that conforms to UIViewRepresentable and UIViewControllerRepresentable protocols.

But this is a long story. If you are curious in implementation, just try to find a couple of examples.

26. Redux in iOS (example of button tapping)

A simple example in which a button increments a counter.

// The state of the application
struct AppState {
    var count: Int = 0
}

// The actions that can be dispatched to the store
enum CounterAction: Action {
    case increment
    case decrement
}

// The reducer, which handles the actions and updates the state
func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()
    switch action {
    case let action as CounterAction:
        switch action {
        case .increment:
            state.count += 1
        case .decrement:
            state.count -= 1
        }
    default:
        break
    }
  
    return state
}

// The store, which holds the state and handles the actions
let store = Store<AppState>(
    reducer: counterReducer,
    state: nil
)

// The view, which displays the state and dispatches actions
struct ContentView: View {
    @ObservedObject var store: Store<AppState>
    var body: some View {
        VStack {
            Text("Count: \(store.state.count)")
            Button("Increment") {
                self.store.dispatch(action: CounterAction.increment)
            }
        }
    }
}

27. Composable architecture

  • View sends events to Action

  • Action sends an action to a Reducer (keeps state of the app alive)

  • Reduces mutate State, sends a call to Effect (outside world), interacts with Environment (dependencies, that are helpful for testing)

  • State influence View

28. What asynchronous functionality is available in Swift?

  • DispatchQueue

  • DispatchGroup

  • Operation queues

  • delegate events

  • Timer operations

  • Combine publisher that is sending data

  • async/await

A note: you should always update interface only on main thread (DispatchQueue.main), otherwise it can just stuck

29. What HTTP methods you know?

  • POST: Sends data to specific server to create or update information.

  • PUT: Sends data to specific server to create or update information without the risk of creating the resource more than once.

  • HEADER: Previews what the GET request response might be without the body of the text.

  • OPTIONS: Learns the communication channels used by the target source.

  • GET: Requests information from a specific source.

  • DELETE: Removes information.

30. How do you test network calls in Unit test?

There are several ways:

  1. You can mock network calls.

protocol NetworkServiceProtocol {
    func getDataFromServer(completion: @escaping (Result<Data, Error>) -> Void)
}

Then, create a mock implementation of the protocol that returns pre-defined data:

class MockNetworkService: NetworkServiceProtocol {
    func getDataFromServer(completion: @escaping (Result<Data, Error>) -> Void) {
        let data = Data("Mocked data".utf8)
        completion(.success(data))
    }
}

Now, in your test case, you can inject the mock network service into your code:

func testGetDataFromServer() {
    let mockService = MockNetworkService()
    let viewModel = MyViewModel(networkService: mockService)
    viewModel.getDataFromServer()
    // Assert that the view model processed the mocked data correctly
    XCTAssertEqual(viewModel.result, "Mocked data")
}
  1. You can mock not only network calls, you can mock entire classes using, for example, OCMock framework

  2. Apple recommends to do something like this, to mock URLSession configuration

https://developer.apple.com/videos/play/wwdc2018/417/

Here is the code example of mocked HTTP request using Combine:

import XCTest

class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
    
    override class func canInit(with request: URLRequest) -&gt; Bool {
        return true
    }

    override class func canonicalRequest(for request: URLRequest) -&gt; URLRequest {
        return request
    }

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            XCTFail("Received unexpected request with no handler set")
            return
        }
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {
    }
}

enum ServiceError: Error, Equatable {
    case invalidURL
    case noInternetConnection
    case requestTimeout
    case networkError
    case statusCodeError(code: Int?)
}

final class NetworkLayerTests: XCTestCase {
    var mockedUrlSession: URLSession!
    
    override func setUpWithError() throws {
        //exit test if something failes
        self.continueAfterFailure = false
        
        let configuration = URLSessionConfiguration.ephemeral
        
        //set up a mock for url session
        configuration.protocolClasses = [MockURLProtocol.self]
        mockedUrlSession = URLSession(configuration: configuration)
    }
  
    struct UserProfile:Codable {
        var name:String?
    }

    class ProfileAPI {
        let url = URL(string: "https://testURL.com/user")!
        private var cancellable: AnyCancellable?
        
        // session to be used to make the API call
        let session: URLSession
        
        // Make the session shared by default.
        // In unit tests, a mock session can be injected.
        init(urlSession: URLSession = .shared) {
            self.session = urlSession
        }
        
        // get user profile from backend
        func getProfile(completion: @escaping (UserProfile) -&gt; Void) {
            cancellable = session.dataTaskPublisher(for: url)
                .mapError { error -&gt; ServiceError in
                    switch error.code {
                    case .notConnectedToInternet:
                        return .noInternetConnection
                    case .timedOut:
                        return .requestTimeout
                    default:
                        return .networkError
                    }
                }
                .tryMap { data, response in
                    guard let httpResponse = response as? HTTPURLResponse,
                        200..&lt;300 ~= httpResponse.statusCode else {
                        throw ServiceError.statusCodeError(code: (response as! HTTPURLResponse).statusCode)
                    }
                    return data
                }
                .decode(type: UserProfile.self, decoder: JSONDecoder())
                .receive(on: RunLoop.main)
                .catch { _ in Just(UserProfile()) }
                .sink { user in
                    completion(user)
            }
        }
    }

    func testCase() throws {
        
        let example = UserProfile(name: "Some User")
        let mockData = try JSONEncoder().encode(example)
        
        //set return data in mock request handler
        MockURLProtocol.requestHandler = { request in
            let response = HTTPURLResponse(url: URL(string: "https://someURL.com/test")!,
                                           statusCode: 200,
                                           httpVersion: nil,
                                           headerFields: ["Content-Type": "application/json"])!
            return (response, mockData)
        }
    
        //this is simpler example, but without http-status mocking
    
    //        MockURLProtocol.requestHandler = { request in
    //            return (HTTPURLResponse(), mockData)
    //        }
        // Set expectation. Used to test async code.
        let expectation = XCTestExpectation(description: "response")
        
        // Make mock network request to get profile
        // here we use the previously set mocked UrlSession
        let profileAPI = ProfileAPI(urlSession: mockedUrlSession)
        
        profileAPI.getProfile { user in
            // Test
            XCTAssertEqual(user.name, "Some User")
            expectation.fulfill()
        }
        wait(for: [expectation], timeout: 1)
    }

}

31. What is the role of the "final" word in the class?

It prevents properties and functions from overriding.

32. What are lazy variables?

They initialize after the first time they are calling.

33. Pros and cons of using UIKit and SwiftUI

34. Is there a difference between "Codable" and "Encodable & Decodable" protocol inheritance?

No, Codable is a type alias for Encodable & Decodable

35. What are Encodable and Decodable protocols used for?

protocol Decodable : allows to decode bytes to the type that inherits Decodable

protocol Encodable : allows to represent a type as data bytes

Often they are used for interaction with JSON and plist.

P.S.: There could be a related question, can we rename keys during encoding/decoding?

Yes, we can using CodingKey syntax

struct Person: Decodable {
    var personName: String
    enum CodingKeys: String, CodingKey {
       case personName = "name"
    }
}

36. How would you explain App Transport Security (ATS) to a junior?

ATS blocks insecure URLSession connections. (Security criteria are shown here https://developer.apple.com/documentation/security/preventing_insecure_network_connections)

There are two ways to prevent connections blocking:

  1. Set "Allow Arbitrary Loads" - it is not a good approach, but it looks like that:

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
  1. You can set the exceptions. That is the good approach. It looks like that:

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>exception.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>

37. How would you explain Dependency Injection to a junior?

Dependency Injection is an approach, when functionality of one entity depends on the other entity and the FIRST gets the SECOND as a parameter.

I would provide an example of MVVM implementation, because link to ViewModel in View is a good example of Dependency Injection.

38. What's difference between @escaping and non-escaping closures?

@escaping launches after the function ends, non-escaping - just after its call.

39. What's difference between inout parameter of the function and a usual parameter?

func example(_ a: Int, _ b: inout Int) {}

inout keyword allows to change the value of the parameter variable.

40. Do classes support multiple inheritance in Swift?

If to try to inherit from multiple classes, then NO. Swift classes don't support multiple inheritance.

But you can inherit multiple protocols.

Некоторые выводы с собеседований

Главные вещи, которые я заметил на собеседованиях:

  1. Скорее всего то, что вы делаете на проекте с технической точки зрения не поможет пройти собеседование, потому как вопросы могут быть из совсем уж разных областей работы с iOS. То есть, готовиться придётся по-любому.

  2. Скорее всего, если вы на собеседовании начали смеяться над чем-то вместе с интервьюером, то вас точно не возьмут, ибо, "не время улыбаться".

  3. Если вы сделали ошибку во время life coding interview, то считайте, что вы завалили интервью:

    Почти все на тренингах по собеседованиям говорят, что если вы не знаете, как решить какую-то проблему или ответить на специфический технический вопрос, то это не значит, что вы не прошли интервью. По факту это именно это и означает. Всегда найдётся кто-то, кто на эти вопросы ответит правильно. Они это отрицают, но что есть, то есть. Поэтому готовьтесь лучше, больше, сильнее.

Репозиторий с новыми вопросами

Чтобы не терять навык я продолжаю периодически проходить собеседования и актуализирую список вопросов в этом репозитории:

https://github.com/AlanMaxwell/iOS_interview_questions

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+2
Комментарии18

Публикации

Истории

Работа

iOS разработчик
25 вакансий
Swift разработчик
41 вакансия

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн