Вопросы для собеседования iOS - Swift. Часть 1

Разница между классом и структурой в Swift:

Структура против класса в Swift

При выборе, что использовать  —  структуру или класс,  —  учитываются различные факторы.

Рекомендация структуры и класса

Шаблон делегирования

Самый важный и частый вопрос на собеседованиях.

Делегирование  —  это шаблон проектирования, согласно которому при возникновении конкретного события от одного объекта другому отправляются сообщения. На Swift он применяется с помощью Protocol.

Шаблон делегирования в Swift

Пример шаблона делегирования с Protocol:

// SomeProtocol.swift
protocol SomeProtocol{ //
func modifyTextToLabel(someText: String)
}

Protocol: создан SomeProtocol.swift и добавлен метод для изменения текста на метку ViewController.

// FirstViewController.swift
class FirstViewController: UIViewController{
private lazy var someLabel: UILabel = {
let label = UILabel()
label.frame = CGRect(x: 100, y: 150, width: 100, height: 50)
label.text = "Default"
return label
}()
private lazy var someButton: UIButton = {
let button = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
button.setTitle("Press ME!", for: .normal)
button.backgroundColor = .red
button.addTarget(self, action: #selector(handleButtonTapped), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(someLabel)
view.addSubview(someButton)
}
}

ViewController: создан FirstViewController и с помощью «ленивых» переменных добавлены метка и кнопка. То есть инициализация или вычисление отложены до тех пор, пока не понадобятся.

// SecondViewController.swift
class SecondViewController: UIViewController, UITextFieldDelegate {
lazy var someTextField: UITextField = {
let textField = UITextField(frame: CGRect(x: 100, y: 100, width: 100, height: 60))
textField.placeholder = "Enter Text"
textField.keyboardType = .default
return textField
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(someTextField)
someTextField.delegate = self
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return true
}
}

ViewController: создан другой ViewController  —  SecondViewController и добавлено текстовое поле для получения пользовательского ввода. someTextField соответствует протоколу UITextFieldDelegate.

class FirstViewController: UIViewController, SomeProtocol {
func modifyTextToLabel(someText: String) {
self.someLabel.text = someText
}
}

FirstViewController соответствует SomeProtocol и унаследовал требования, или методы.

class SecondViewController: UIViewController, UITextFieldDelegate {
weak var delegate: SomeProtocol? // слабая переменная во избежание циклов сохранения

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField.hasText {
delegate?.modifyTextToLabel(someText: textField.text ?? "")
navigationController?.popToRootViewController(animated: true)
return true
}
return false
}
}

В переменной «delegate» содержится SomeProtocol, она как делегат между SecondViewController и FirstViewController. Присвоена слабой ссылке во избежание циклов сохранения.

Здесь modifyTextToLabel  —  это метод делегата для передачи данных из SecondViewController в FirstViewController. Поэтому текст этого someLabel в FirstViewController изменится.

А почему в Protocol используется AnyObject?

Из-за управления памятью: мы сохранили объект делегата в слабом свойстве. Однако сталкиваемся с проблемой, когда в Xcode выбрасывается ошибка и код не запускается.

'weak' must not be applied to non-class-bound 'MyDelegate'; 
consider adding a protocol conformance that has a class bound

Чтобы ее устранить, нужен протокол только для классов:

protocol SomeProtocol: AnyObject {
func modifyTextToLabel(someText: String)
}

Слабые ссылки применимы только к ссылочным типам, то есть типы значений вроде структур или перечислений не соответствуют этому протоколу SomeProtocol.

Если объект, соответствующий протоколу, необходимо сохранить в слабом свойстве, это должен быть протокол только для классов.

Каким проверкам времени выполнения вы следуете в проекте на Swift?

Во время разработки iOS, прежде чем выпускать приложение в App Store, обязательно выполняются все проверки работоспособности:

1. Предусловие.
2. Утверждение.
3. Неустранимая ошибка.
4. Guard.

Предусловие

Прежде чем приступать к коду, проверяется условие. Если условием оценки не пройдены, выполнение программы останавливается.

Хорошо справляется и в продакшене, и в разработке.

func preCondition(willRun: Bool) {
precondition(willRun)
print("Called \(#function) successfully")
}
preCondition(willRun: true) // Успешно вызвано «preCondition(willRun:)»
preCondition(willRun: false) // Предусловие не выполнено: значение «willRun» должно быть «true»

func preConditionFailure(){
preconditionFailure("It will not run")
}
preConditionFailure() // Неустранимая ошибка: программа не выполнится

Утверждение

Эта проверка хороша в разработке через тестирование. Проблема устраняется во время разработки и на ранней стадии.

Хорошо справляется в разработке.

func assertion(willRun: Bool) {
assert(willRun)
print("Called \(#function) successfully")
}
assertion(willRun: true) // Успешно вызвано «assertion(willRun:)»
assertion(willRun: false) // Утверждение не пройдено

func assertionFail() {
assertionFailure("Expected Failure")
print("Called \(#function) successfully")
}
assertionFail() // Неустранимая ошибка: ожидаемый сбой

Неустранимая ошибка

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

Если это используется в коде на продакшене, в Apple приложение не одобрят.

Хорошо справляется в разработке.

func testfatalError() {
fatalError("FatalError")
}
testfatalError() // Неустранимая ошибка: «FatalError»

«fatalError()» используется в «TableView»:

tableView(_:cellForRowAtIndexPath:)

Guard

Оператором «Guard» определяются различные пути: true или false.

Он применяется в сценариях:

  1. Раннего завершения при невыполнении предусловий.
  2. Счастливого пути при раннем возвращении.
  3. Видимости.
  4. В циклах for и while.

Хорошо справляется и в продакшене, и в разработке.

func testGuard(willCall: Bool) {
guard willCall else {
print("No Call"
return
}
print("Called \(#function) successfully")
}
testGuard(willCall: true) // Успешно вызван «testGuard(willCall:)»
testGuard(willCall: false) // Нет вызова

Зачем приводить «NSObject» в соответствие классу Swift? Необходимо ли это делать?

Прежде чем ответить на этот вопрос, разберемся, нужен ли NSObject.

Что такое «NSObject»?

NSObject  —  общепризнанный базовый класс для всех классов Cocoa Touch, включая все производные от него классы UIKit.

NSObject  —  это абстрактный класс.

В Swift не нужно наследовать UIKit от NSObject, но мы сделали это с помощью Objective-C.

Преимущества: созданием подкласса NSObject в Swift привносится гибкость среды выполнения Objective-C; ключевые механизмы вроде NSCoding, KVO и Hashable.

Недостаток: производительность Objective-C, Swift быстрее.

Для чистого программирования на Swift следует избегать создания подкласса NSObject.

Что такое «ПОП»?

Это протокольно-ориентированное программирование, первым в мире языком которого компанией Apple на WWDC 2015 объявлен Swift.

Определение протокола

Протокол  —  это набор методов, свойств, индексов и т. д. под конкретную задачу или функциональность, принимаемый классами, структурами или перечислениями для обеспечения фактической реализации требований.

При соблюдении требований говорят о соответствии протоколу. На протоколах основана вся библиотека Swift.

protocol Epics {
func isAMovie()
}

protocol Myths {
func isAMyth()
}

Расширения

Свойства или методы имеющихся типов расширяют расширениями без изменения фактической реализации. С помощью расширений получаются дополнительные методы.

Расширение  —  реализация по умолчанию для любого свойства или метода. Переопределяются методы, определенные в родительском классе:

extension Epics {
func isABook() {
print("Yes, isABook")
}
}

Если протокол соответствует другому протоколу, он соответствует всем его требованиям. С помощью протокола получают множественное наследование.

Отличные примеры протоколов  —  их наследование и композиция.

Наследование протоколов и их композиция

Прежде чем переходить к этой теме, вернемся с базовым понятиям.

Что такое «наследование»?

Наследование  —  это парадигма ООП, которая означает «создание подкласса из суперкласса».
Свойства или методы переопределяются подклассом за счет изменения поведения по умолчанию.

Поддерживается ли типами значений наследование? Нет. Структурами или перечислениями наследование не поддерживается.

Что такое «композиция»?

Композиция  —  это комбинация нескольких частей для создания единого результата.

Сочетаются ли структуры с композицией? Структура хорошо сочетается с композицией: с ней выше производительность, безопасность при работе с памятью.

Наследование протоколов

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

protocol FirstProtocol { 
var firstName: String {get}
}
protocol SecondProtocol {
var lastName: String {get}
}
struct Person: FirstProtocol, SecondProtocol {
var firstName: String = "First Name"
var lastName: String = "Last Name"
}

let person = Person()
print(person.firstName)
print(person.lastName)

Композиция протоколов

Это объединение нескольких протоколов в единое требование без определения нового типа или новой иерархии протоколов. Так получают множественное наследование.

Перечисляется сколько угодно протоколов, они разделяются символом амперсанда:

protocol FirstProtocol { 
var firstName: String {get}
}
protocol SecondProtocol {
var lastName: String {get}
}
struct Person: FirstProtocol & SecondProtocol {
var firstName: String = "First Name"
var lastName: String = "Last Name"
}

let person = Person()
print(person.firstName)
print(person.lastName)

Применяемый в Swift подход

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

typealias SingleProtocol = FirstProtocol & SecondProtocol
protocol FirstProtocol {
var firstName: String { get }
}
protocol SecondProtocol {
var secondName: String { get }
}
struct NewStruct: SingleProtocol {
var firstName: String = "FirstName"
var secondName: String = "SecondName"
}

let newStruct = NewStruct()
print(newStruct.firstName)
print(newStruct.secondName)

Что такое «псевдоним типа»?

Это функция, в которой имеющемуся типу  —  конкретному типу, сложной или пользовательской структуре, типу замыкания  —  дается новое имя или псевдоним. Так код становится удобнее для восприятия, проще в сопровождении.

Выбор между наследованием протоколов и их композицией

Композиция протоколов используется при соответствии типа нескольким протоколам одновременно, и без появления новой иерархии.

Наследование протоколов  —  для создания нового протокола на основе требований имеющегося, и с расширением функциональности последнего.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Kanagasabapathy Rajkumar: iOS — Swift Interview Questions 2023

Предыдущая статьяRust: безопасный парсинг с нулевым копированием
Следующая статьяЭтические проблемы в науке о данных