
Привет, Хабр! Сегодня мы в коротком формате сравним SwiftUI и UIKit: где SwiftUI реально выигрывает, а где старый добрый UIKit остаётся незаменимым.
Архитектурные различия
UIKit
В UIKit вы вручную создаёте экземпляры UIViewController, добавляете UIView, настраиваете AutoLayout (или пишете frame-based лэйауты, если душа требует свободы) и связываете события через делегаты или target-action.
Например, форма авторизации на UIKit может выглядеть так:
import UIKit
class LoginViewController: UIViewController {
private let usernameField: UITextField = {
let field = UITextField()
field.placeholder = "Логин"
field.borderStyle = .roundedRect
return field
}()
private let passwordField: UITextField = {
let field = UITextField()
field.placeholder = "Пароль"
field.borderStyle = .roundedRect
field.isSecureTextEntry = true
return field
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Войти", for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(usernameField)
view.addSubview(passwordField)
view.addSubview(loginButton)
setupConstraints()
}
private func setupConstraints() {
usernameField.translatesAutoresizingMaskIntoConstraints = false
passwordField.translatesAutoresizingMaskIntoConstraints = false
loginButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
usernameField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
usernameField.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
usernameField.widthAnchor.constraint(equalToConstant: 250),
passwordField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
passwordField.topAnchor.constraint(equalTo: usernameField.bottomAnchor, constant: 20),
passwordField.widthAnchor.constraint(equalTo: usernameField.widthAnchor),
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loginButton.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: 20)
])
}
}
Здесь не только создаётся три элемента, но ещё и вручную настраиваются констрейнты. Контроль полный, но код получается многословным и порой бессмысленно шаблонным.
SwiftUI
SwiftUI предлагает совершенно другой взгляд – вы описываете, что должно быть на экране, и система сама решает, как это отрисовать.
Пример той же формы авторизации на SwiftUI:
import SwiftUI
struct LoginView: View {
@State private var username = ""
@State private var password = ""
var body: some View {
VStack(spacing: 20) {
TextField("Логин", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal, 20)
SecureField("Пароль", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal, 20)
Button("Войти") {
// Логика авторизации
print("Попытка входа с логином \(username)")
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
Как видите, код становится компактнее, понятнее и, что самое главное, декларативнее. Вам не нужно заботиться о констрейнтах – SwiftUI сам позаботится о лэйауте. Но вот за кажущейся простотой скрываются алгоритмы diffing-а, которые при каждом изменении состояния пересчитывают всё дерево представлений.
Производительность
Как видите, код становится компактнее, понятнее и, что самое главное, декларативнее. Вам не нужно заботиться о констрейнтах – SwiftUI сам позаботится о лэйауте. Но вот за кажущейся простотой скрываются алгоритмы diffing-а, которые при каждом изменении состояния пересчитывают всё дерево представлений.
Производительность
UIKit
UIKit напрямую работает с CALayer и Core Animation. Это даёт возможность оптимизировать производительность на уровне отрисовки, кешировать содержимое, управлять GPU-ресурсами. Если есть огромный список данных, используется UITableView или UICollectionView с механизмом повторного использования ячеек. Можно вручную настроить estimatedRowHeight и rowHeight, чтобы добиться оптимального баланса между производительностью и точностью лэйаута:
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
Кроме того, можно проследить цепочку отрисовки через инструменты типа Instruments, анализируя каждую фазу и устраняя узкие места.
SwiftUI
SwiftUI работает на основе механизма, который сравнивает старое и новое дерево представлений при изменении состояния. Если вы не оптимизируете список с помощью правильного указания id или использования модификаторов типа EquatableView, система может пересоздавать большое количество элементов, что приводит к лишним пересчётам. Вот типичная ошибка:
List(0..<1000) { index in
Text("Элемент \(index)")
}
Каждое изменение состояния заставляет SwiftUI пересчитывать все 1000 элементов. Правильное решение – указать идентификатор:
List(0..<1000, id: \.self) { index in
Text("Элемент \(index)")
}
Это позволяет системе понять, что элементы не изменились, и избежать ненужной перерисовки.
Анимации
UIKit
В UIKit анимации строятся через UIView.animate и Core Animation. Можно задавать свои кривые, управлять задержками, комбинировать анимации и контролировать продолжительность и синхронизацию. Это позволяет создавать сложные визуальные эффекты, требующие точной настройки:
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut], animations: {
self.view.alpha = 0.5
}, completion: nil)
SwiftUI
SwiftUI предлагает анимации через обёртку withAnimation – изменяете состояние, и все изменения плавно перерисовываются.
withAnimation {
opacity = 0.5
}
Но если нужно что-то более сложное – например, кастомные кривые анимации, последовательные переходы или контроль над фазами анимации – может понадобиться уйти в UIKit или использовать комбинированный подход. В SwiftUI анимации зачастую ограничены декларативным описанием, и для нестандартных эффектов приходится прибегать к трюкам или даже интегрировать UIViewRepresentable для встраивания UIKit-компонентов.
Управление состоянием
Одним из главных козырей SwiftUI является его встроенная система управления состоянием. Используя свойства типа @State, @Binding, @ObservedObject и @EnvironmentObject, можно буквально забыть о том, что происходит – система сама обновляет интерфейс при изменении данных. Это выглядит очень и просто, но, как известно, за такой декларативной простотой скрывается тонкая настройка. Если не структурировать данные правильно, можно столкнуться с лишними обновлениями, или с зацикливанием обновлений, когда одно изменение запускает цепную реакцию перерасчётов.
Пример наблюдаемого объекта в SwiftUI:
import SwiftUI
import Combine
class UserViewModel: ObservableObject {
@Published var username: String = ""
@Published var isLoggedIn: Bool = false
}
Здесь все обновления происходят автоматически: как только вы изменяете username или isLoggedIn, все представления, зависящие от этих данных, перерисовываются.
В UIKit ситуация совсем иная. Здесь управление состоянием зачастую реализуется через делегаты, нотификации или архитектурные паттерны вроде MVC и MVVM.
Например, рассмотрим два способа отслеживания изменений в UIKit.
Создадим модель с делегатом, чтобы уведомлять об изменениях:
protocol UserModelDelegate: AnyObject {
func userModelDidUpdate(_ model: UserModel)
}
class UserModel {
weak var delegate: UserModelDelegate?
var username: String {
didSet {
delegate?.userModelDidUpdate(self)
}
}
init(username: String) {
self.username = username
}
}
А теперь – UIViewController, который будет следить за изменениями:
import UIKit
class UserViewController: UIViewController, UserModelDelegate {
var userModel = UserModel(username: "Начальное имя")
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
userModel.delegate = self
// Симулируем обновление через 2 секунды
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.userModel.username = "Новое имя"
}
}
func userModelDidUpdate(_ model: UserModel) {
print("Делегат: имя пользователя изменилось на \(model.username)")
// Здесь можно обновить UI, если требуется
}
}
Если хотите еще большей автоматизации без явных вызовов делегата, можно использовать KVO:
import Foundation
class UserModel: NSObject {
@objc dynamic var username: String
init(username: String) {
self.username = username
super.init()
}
}
Настроим наблюдение в контроллере:
import UIKit
class UserViewController: UIViewController {
var userModel = UserModel(username: "Начальное имя")
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
observation = userModel.observe(\.username, options: [.old, .new]) { model, change in
if let oldValue = change.oldValue, let newValue = change.newValue {
print("KVO: имя пользователя изменилось с \(oldValue) на \(newValue)")
}
}
// Симулируем обновление через 2 секунды
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.userModel.username = "Новое имя"
}
}
deinit {
observation?.invalidate()
}
}
В обоих случаях получаем полный контроль над моментами обновления, хотя и жертвуете простотой декларативного подхода SwiftUI. Разница в том, что в UIKit ответственность за управление состоянием лежит на вас – и это может быть как преимуществом, так и источником ошибок, если не уделить должного внимания оптимизации.
Кстати, на основе этого опыта наши приложения М.Видео и Эльдорадо обновлены и снова доступны в AppStore – мы интегрировали современные технологии, сохранив проверенную временем стабильность.
Теперь у нас:
Обновлённый дизайн – используя гибридный подход UIKit и SwiftUI.
Быстрая работа – оптимизированная загрузка страниц, умное кеширование.
Новый поиск – лучше, быстрее, удобнее.
Поддержка бонусов – единый счёт в М.Клубе.
Так что если у вас iPhone – пора обновить приложение!
Скачать приложение М.Видео в AppStore. Скачать Эльдорадо в AppStore.
Спасибо, что дочитали до конца!