Irbisa · Шпаргалка

Основы UIKit

UIView, жизненный цикл UIViewController, иерархия view, обработка касаний

Junior-разработчик · 25 вопросов

  1. 01

    Что такое UIWindow, с чего начинается иерархия вью и чем окно отличается от обычного UIView?

    UIWindow служит корнем иерархии вью сцены: подкласс UIView без superview, который держит rootViewController, получает события от UIApplication и передаёт их вью внутри себя.

  2. 02

    Расскажите о жизненном цикле UIViewController: какие колбэки вызываются один раз, а какие при каждом появлении, и где начинать загрузку данных?

    loadView и viewDidLoad вызываются один раз на экземпляр контроллера; viewWillAppear, viewIsAppearing, viewDidAppear и два колбэка исчезновения вызываются каждый раз, когда экран показывается или скрывается.

  3. 03

    Что такое loadView, когда его переопределяют и чем он отличается от viewDidLoad?

    loadView создаёт корневую вью контроллера и присваивает её в view; viewDidLoad вызывается сразу после него и настраивает уже существующую вью.

    final class ProfileView: UIView {  let nameLabel = UILabel()  // builds its own subviews and constraints in init} final class ProfileViewController: UIViewController {…
  4. 04

    Чем отличаются frame и bounds и когда bounds.origin не равен нулю?

    frame задаёт положение и размер вью в системе координат superview; bounds описывает тот же прямоугольник в собственных координатах вью.

    let card = UIView(frame: CGRect(x: 20, y: 100, width: 120, height: 60))// card.frame  = (20, 100, 120, 60)  in the superview's coordinates// card.bounds = (0, 0, 120, 60)     in its own coordinates let dot = UIView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))card.addSubview(dot)                 // (10, 10) inside card…
  5. 05

    Каковы плюсы и минусы сторибордов, XIB и вёрстки интерфейса в коде?

    Все три способа дают одни и те же вью UIKit: сториборды и XIB предлагают визуальный редактор, а код даёт читаемые диффы и заметно меньше конфликтов при слиянии.

  6. 06

    Чем отличаются @IBOutlet и @IBAction и как target-action связывает контрол с вашим кодом?

    @IBOutlet помечает свойство, которое Interface Builder заполняет ссылкой на вью, чтобы код мог её читать и менять; @IBAction помечает метод, который Interface Builder привязывает к событию контрола, чтобы контрол мог вызвать ваш код.

    final class LoginViewController: UIViewController {  @IBOutlet private weak var emailField: UITextField!  @IBOutlet private weak var loginButton: UIButton!  private let retryButton = UIButton(type: .system)   @IBAction private func loginTapped(_ sender: UIButton) {…
  7. 07

    Как работает UINavigationController и чем push отличается от present?

    UINavigationController управляет стеком view controller'ов: push кладёт контроллер наверх с кнопкой «Назад» и свайпом назад, pop снимает его.

    // Push: onto the current navigation stacknavigationController?.pushViewController(DetailViewController(id: id), animated: true)navigationController?.popViewController(animated: true) // Present: modally, with its own navigation barlet editor = UINavigationController(rootViewController: EditorViewController())…
  8. 08

    Как работает UITableView: что делают data source и delegate и что такое переиспользование ячеек?

    Таблица спрашивает у data source, сколько строк, и ячейку для каждой видимой строки, а delegate сообщает о выборе, размерах и прокрутке.

    final class ContactsViewController: UITableViewController {  private var names = ["Alice", "Bob", "Carol"]   override func viewDidLoad() {    super.viewDidLoad()    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")…
  9. 09

    Как устроен UIScrollView и за что в нём отвечают свойства contentSize, contentOffset и contentInset?

    Scroll view представляет собой вью, у которой bounds.origin сдвигается, когда пользователь тянет пальцем: contentSize задаёт размер прокручиваемого содержимого, contentOffset показывает, какая его точка находится в левом верхнем углу, а contentInset добавляет дополнительное прокручиваемое пространство вокруг.

    final class FeedViewController: UIViewController, UIScrollViewDelegate {  private let scrollView = UIScrollView()  private let header = UIView()   override func viewDidLoad() {    super.viewDidLoad()…
  10. 10

    Чем отличаются UIImage и UIImageView, а также .scaleAspectFit и .scaleAspectFill?

    UIImage хранит данные изображения (пиксели и масштаб), которые не находятся на экране; UIImageView является вью, которая показывает их согласно contentMode.

  11. 11

    Как скруглить углы вью и добавить тень и почему они конфликтуют?

    Скругляйте через layer.cornerRadius и clipsToBounds, а тень задавайте через layer.shadowOpacity/shadowRadius/shadowOffset; обрезка отсекает тень, поэтому нужны две вью: внешняя с тенью и внутренняя, которая обрезает.

    final class CardView: UIView {  let content = UIView()                 // inner view: clips to the rounded shape   override init(frame: CGRect) {    super.init(frame: frame)    layer.shadowColor = UIColor.black.cgColor…
  12. 12

    Этот лейбл обновляется из колбэка URLSession, и приложение потом падает в случайных местах. Что не так и как это исправить?

    func loadProfile() {  URLSession.shared.dataTask(with: profileURL) { data, _, _ in    guard let data else { return }    let profile = try? JSONDecoder().decode(Profile.self, from: data)    self.nameLabel.text = profile?.name    self.view.setNeedsLayout()  }.resume()}

    Completion handler у dataTask выполняется на фоновой очереди, а UIKit можно использовать только на главном потоке.

  13. 13

    Как поддержать тёмную тему в UIKit и почему цвет рамки слоя не переключается вместе с ней?

    Используйте динамические цвета (системные семантические вроде .label и .systemBackground или цвета из asset catalog с тёмным вариантом), и UIKit сам пересчитывает их при каждой смене оформления.

    final class BadgeView: UIView {  override init(frame: CGRect) {    super.init(frame: frame)    backgroundColor = .secondarySystemBackground   // dynamic: updates itself    layer.borderWidth = 1    updateBorder()…
  14. 14

    Как анимировать вью в UIKit и как анимировать изменение констрейнта Auto Layout?

    Поместите изменения свойств внутрь UIView.animate(withDuration:animations:).

    // Expand a panel by changing its height constraintview.layoutIfNeeded()                        // settle pending layout firstheightConstraint.constant = isExpanded ? 240 : 88UIView.animate(withDuration: 0.3) {  self.view.layoutIfNeeded()                 // the layout pass is animated}…
  15. 15

    Что такое viewIsAppearing(_:) и почему для настройки, зависящей от геометрии, он лучше, чем viewWillAppear или viewDidLayoutSubviews?

    viewIsAppearing вызывается один раз за появление: после того как вью оказалась в иерархии с окончательным размером и трейтами, но до того как она на экране.

    final class AgendaViewController: UIViewController {  private let tableView = UITableView()  private var todayIndexPath = IndexPath(row: 0, section: 0)   // wrong: scrolling in viewWillAppear, bounds and traits may be stale  override func viewWillAppear(_ animated: Bool) {…
  16. 16

    Найдите баг: после нескольких заходов на экран корзины одно изменение корзины попадает в аналитику несколько раз.

    final class CartViewController: UIViewController {  override func viewWillAppear(_ animated: Bool) {    super.viewWillAppear(animated)    NotificationCenter.default.addObserver(      self, selector: #selector(cartDidChange),      name: .cartDidChange, object: nil)    reloadBadge()  }   @objc private func cartDidChange() {    reloadBadge()    analytics.log("cart_changed")  }}

    Наблюдатель добавляется во viewWillAppear, который вызывается при каждом появлении экрана, и нигде не удаляется.

  17. 17

    Как передать данные вперёд, в следующий view controller, и назад, в предыдущий?

    Вперёд: отдайте следующему контроллеру всё нужное до его показа, лучше всего через init.

    struct Item { var title: String } final class EditViewController: UIViewController {  private var item: Item  var onSave: ((Item) -> Void)?              // back: a callback the parent sets…
  18. 18

    Что такое containment view controller'ов и какие вызовы нужны, чтобы добавить и убрать дочерний контроллер?

    Containment позволяет одному view controller'у встроить вью другого в свою, при этом дочерний остаётся полноценным контроллером со своим жизненным циклом.

    extension UIViewController {  func embed(_ child: UIViewController, in container: UIView) {    addChild(child)                                   // 1. relationship    child.view.translatesAutoresizingMaskIntoConstraints = false    container.addSubview(child.view)                  // 2. view    NSLayoutConstraint.activate([…
  19. 19

    Что такое responder chain и как hitTest(_:with:) определяет, какая вью получит касание?

    UIKit сначала выполняет hit-testing от окна вниз, чтобы найти самую глубокую вью под пальцем, и доставляет касание ей; всё, что эта вью не обработала, поднимается по responder chain: superview, view controller, окно, приложение.

    // Enlarge a small button's tap area without changing its sizefinal class BigTapButton: UIButton {  override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {    bounds.insetBy(dx: -12, dy: -12).contains(point)  }}…
  20. 20

    Как работают gesture recognizers и как сделать, чтобы одиночный тап ждал двойной, а pan работал вместе со scroll view?

    Gesture recognizer представляет собой машину состояний, прикреплённую к вью: он следит за касаниями и вызывает свой action, когда распознаёт жест.

    final class CardViewController: UIViewController, UIGestureRecognizerDelegate {  private let scrollView = UIScrollView()  private let card = UIView()  private var dragStart: CGPoint = .zero  override func viewDidLoad() {    super.viewDidLoad()…
  21. 21

    Чем отличаются UIView и CALayer и когда работают со слоем напрямую?

    Каждый UIView опирается на CALayer: вью отвечает за касания, layout, responder chain и accessibility, а слой хранит содержимое, которое рисует и анимирует Core Animation.

    final class GradientView: UIView {  // The backing layer itself is a gradient: no frame syncing needed  override class var layerClass: AnyClass { CAGradientLayer.self }  private var gradient: CAGradientLayer { layer as! CAGradientLayer }   override init(frame: CGRect) {…
  22. 22

    Чем отличаются setNeedsLayout и setNeedsDisplay и когда вызывается draw(_:)?

    setNeedsLayout помечает вью для нового прохода layout, и вызывается layoutSubviews; setNeedsDisplay помечает её содержимое устаревшим, и вызывается draw(_:) для перерисовки.

    final class ProgressRingView: UIView {  var progress: CGFloat = 0 {    didSet { setNeedsDisplay() }          // content changed: redraw once  }   override init(frame: CGRect) {…
  23. 23

    Как работает главный run loop и почему Timer перестаёт срабатывать, пока пользователь скроллит?

    Главный run loop служит циклом событий главного потока: он обрабатывает касания, таймеры и блоки из очереди, затем коммитит layout и отрисовку.

    // wrong: scheduledTimer registers for .default only, so it pauses while scrolling// Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in ... } // right: register for .common, which includes .tracking// tick() is a method of the owning controllerlet countdown = Timer(timeInterval: 1, repeats: true) { [weak self] _ in…
  24. 24

    Этот контроллер закрывают через 3 секунды после открытия. Что напечатается, когда и есть ли здесь утечка памяти?

    final class DetailViewController: UIViewController {  private let statusLabel = UILabel()   override func viewDidLoad() {    super.viewDidLoad()    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {      self.statusLabel.text = "Loaded"      print("closure ran")    }  }   deinit { print("deinit") }}

    Через 3 секунды ничего не напечатается.

  25. 25

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

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {  let cell = tableView.dequeueReusableCell(withIdentifier: "Avatar", for: indexPath) as! AvatarCell  let user = users[indexPath.row]  cell.nameLabel.text = user.name  Task {    let image = await imageLoader.image(for: user.avatarURL)    cell.avatarView.image = image  }  return cell}

    Task захватывает ячейку, а не строку.