Привет, Хабр! Меня зовут Алексей Непомнящих и я мобильный разработчик Леруа Мерлен. В этой статье я бы хотел поделиться своим опытом внедрения первой большой фичи на SwiftUI в приложение, целиком состоящее из UIKit с минимальной требуемой версией iOS 14.

Содержание статьи

  1. Выбор в пользу SwiftUI: первые шаги и ожидания от перехода

  2. Заметки новичка: первые трудности работы с SwiftUI

  3. Открытие новых горизонтов: погружение в мир SwiftUI. Поможет ли нам ChatGPT?

  4. Пара слов про архитектуру

  5. Комбинирование SwiftUI и UIKit: преодоление технических преград

  6. Сюрпризы на пути: неожиданные ошибки и способы их решения

  7. Уроки и открытия: полезные находки и применение лучших практик

  8. Интеграция SwiftUI и UIKit: результаты

  9. Итоги: мои новые взгляды на SwiftUI

Выбор в пользу SwiftUI: первые шаги и ожидания от перехода

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

Имеющийся вариант отображения списком
Имеющийся вариант отображения списком
Новый вариант отображения плиткой
Новый вариант отображения плиткой

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

Увидев новый дизайн, сначала мы решили, что будем переписывать на UICollectionView. Или на худой конец оставим UITableView, где в одной ячейке будет два товара.

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

 Вот так DALLE видит запутанный и старый код
Вот так DALLE видит запутанный и старый код

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

Наше текущее приложение представляет собой монорепозиторий с клиентами под Android и iOS, их объединяет общая логика на KMM, а UI на Android верстается через Compose. Использование похожего декларативного подхода больше сблизит приложения под iOS и Android, а общую логику получится использовать одинаково декларативно.

Заметки новичка: первые трудности работы с SwiftUI

Трудности начались буквально сразу. В основном проекте ни в какую не запускалось SwiftUI Preview.

На финальных стадиях написании статьи нашлось несколько решений, которые помогли справиться с проблемой:

  1. Самое простое и то, которое помогло — это просто отключить галочку Xcode — Editor — Canvas — Automatically Refresh Canvas.

2. Можно вдохновиться этим тредом и попробовать поправить Podfile.

3. Посмотреть логи ошибки: найти используемые фреймворки и попробовать использовать их динамически, если они использовались статически.

Однако тогда в качестве временного решения было выбрано использовать проект дизайн-системы: он новее, легче, и в нем Preview работало как надо. Тем более, в проекте дизайн-системы уже есть компоненты, цвета и шрифты –  буквально все, что нужно для верстки.

Открытие новых горизонтов: погружение в мир SwiftUI

Переход на SwiftUI означал, что старый вариант отображения тоже придется переписать, хоть он и не должен был меняться. Казалось, что сделать это будет несложно: компоненты дизайн-системы мы обернем в UIViewRepresentable, а еще возьмем имеющийся Compose код и попросим ChatGPT перевести на SwiftUI, делов то!

Это оказалось недалеко от истины. ChatGPT действительно помог решить проблему чистого листа и накидать приблизительную структуру, которую потом можно было допиливать. Что-то начало получаться.

 Бриф-перевод Compose на SwiftUI с помощью ChatGPT, не без ручных правок, конечно, далеко не без них…
Бриф-перевод Compose на SwiftUI с помощью ChatGPT, не без ручных правок, конечно, далеко не без них…

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

Комбинирование SwiftUI и UIKit: преодоление технических преград

После верстки первой партии вьюшек пришла пора проверить их в реальном окружении, на реальных данных.

Не секрет, что Apple для использования SwiftUI-представлений в контексте UIKit предоставила функционал UIViewRepresentable. Используется он обычно примерно так.

import SwiftUI

func routeToSampleScreen() {
    let sampleScreen = SampleScreen() // Creating an instance of SampleScreen
    let hostingController = UIHostingController(rootView: sampleScreen) // Wrapping it in a UIHostingController
    viewController?.present(hostingController, animated: true, completion: nil) // Presenting the hosting controller
}

Но возникло несколько проблем:

  • Экран должен открываться одинаково из нескольких мест приложения, и дублировать инициализацию UIHostingController нашим новым SwiftUI-экраном немного опасно;

  • Встраивание в UIKit-контекст подразумевает UIKit-based роутинг других действий c новоиспеченного SwiftUI-экрана на имеющиеся;

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

Как я уже сказал ранее, приложение представляет собой монорепозиторий с клиентами для iOS и Android. Эти клиенты объединены и используют архитектуру с общей ViewModel, которая написана на языке Kotlin и после может быть использована как на Android, так и на iOS. Вы тоже можете ее попробовать, она open source. 

В итоге получился такой скелет экрана на SwiftUI. Он имеет свой роутер, об этом дальше. 

import SwiftUI

private typealias VMWrapper = ViewModelWrapper<
    ExampleViewState,
    ExampleAction,
    ExampleEvent
>

struct ExampleScreen: View {
    private var viewModel: VMWrapper
    private let router: ExampleScreenHostingRouter

    @State
    private var viewState: ExampleViewState

    init(
        viewModel: ExampleViewModel,
        router: ExampleScreenHostingRouter
    ) {
        let viewModel = VMWrapper(viewModel: viewModel)
        self.viewModel = viewModel
        self.router = router
        self.viewState = viewModel.currentViewState
    }

    var body: some View {
        Text("Hello, world!")
        .onReceive(viewModel.viewActionPublisher) { (action: ExampleAction) in
            performAction(action: action)
        }.onReceive(viewModel.viewStatePublisher) { (newViewState: ExampleViewState) in
            viewState = newViewState
        }
    }

    func performAction(action: ExampleAction) {

        switch action {
        case is ExampleAction.Idle:
            return
        case is ExampleAction.CloseScreen:
            router.exit()
        default:
            fatalError("Not implemented \(action) case.")
        }
    }
}

Его мы обернули в UIHostingController. Здесь получилась четкая связь с конкретной SwiftUI-вьюшкой. Инициализируем ее и поставляем роутер от UIKit, чтобы по-максимуму выдавить логику из viewController-обертки.

import SwiftUI

final class ExampleScreenHostingController: UIHostingController<ExampleScreen> {

    private let viewModel: ExampleViewModel

    // MARK: - External vars
    private(set) var router: ExampleScreenHostingRouter?

    // MARK: Object lifecycle
    init() {
        viewModel = ExampleViewModel()
        let router = ExampleScreenHostingRouter()

        super.init(
            rootView: ExampleScreen(
                viewModel: viewModel,
                router: router
            )
        )

        router.viewController = self
    }

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        viewModel.clear()
    }

    // MARK: View lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.obtainEvent(viewEvent: .OnCreate())
    }
}

Заканчиваем навигацией UIKit через Router. Здесь уже совсем нет связи с SwiftUI, лишь осуществляются переходы на другие экраны. 

import UIKit

final class ExampleScreenHostingRouter {

    // MARK: Internal vars
    weak var viewController: UIViewController?
}

extension ExampleScreenHostingRouter {

    func routeToPdpAdditionalInfoScreen() {
        let newVC = NewViewController()
        viewController?.navigationController?.pushViewController(newVC, animated: true)
    }

    func exit() {
        viewController?.exit()
    }
}

В результате у нас получился SwiftUI‑экран, который имеет экземпляр UIKit‑роутера и может открывать другие экраны, получая новые события от ViewModel.

Билдим, запускаем!

Сюрпризы на пути: неожиданные ошибки и способы их решения

Первое, с чем я столкнулся, когда запустил приложение со сверстанным экраном на моках, — контент почему‑то был шире, чем в превью. Поначалу казалось, что UIHostingController неправильно считает ширину, ведь в preview все было правильно. На популярных ресурсах даже нашлись решения этой проблемы. Однако мы обошлись старым дедовским методом — поэтапным комментированием частей кода до тех пор, пока отображение не приняло ожидаемый вид. Все дело было в неправильно заданных размерах в одной из сабвью.

Далее, когда отображение было уже почти готово, логика — накинута и казалось, что скоро фича будет доделана, обнаружился странный баг. Выглядел он примерно так. При скролле вверх, как при pull to refresh, появлялась очень странная анимация отпускания. Запись экрана, к сожалению, не смогла запечатлеть, но выглядело это как натянутая гитарная струна, которая очень быстро перемещается в пространстве. Хотя даже это — мелочи по сравнению с главной проблемой, которую пришлось решить. Заключалась она в том, что экран лагал, иногда так сильно, что телефон зависал. Пришлось в этом разбираться.

Оптимизация верстки на SwiftUI

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

Есть два способа реализовать бесконечный скролл на SwiftUI: LazyVStack + ScrollView и List. List — это что‑то типа UITableView, он умеет переиспользовать ячейки и имеет огромное количество визуальных стилей от UITableView: разделители ячеек, их группирование и все прочее. Связка LazyVStack + ScrollView переиспользовать ячейки не умеет, но умеет создавать новые только тогда, когда это необходимо. Давайте рассмотрим плюсы и минусы каждого из них.

List

Плюсы:

  1. Эффективное Управление Памятью. Автоматическое переиспользование ячеек в List снижает потребление памяти. Это особенно важно при работе с большими объемами данных.

  2. Ленивая Загрузка. Элементы загружаются по мере необходимости, что уменьшает начальную нагрузку и ускоряет отклик приложения.

  3. Плавная Прокрутка. Оптимизированная прокрутка обеспечивает лучший пользовательский опыт даже при большом количестве элементов.

Минусы:

  1. Ограниченная кастомизация. Сложно изменить стандартные стили UITableView, которые использует List. Например, при размещении вью в Section header весь текст автоматически пишется верхним регистром и это нельзя отключить.

  2. Меньшая Гибкость в Разметке. List менее гибок по сравнению с комбинацией LazyVStack + ScrollView в создании сложных пользовательских интерфейсов.

LazyVStack + ScrollView

Плюсы:

  1. Высокая Гибкость в Дизайне. Позволяет создавать более сложные и кастомизируемые пользовательские интерфейсы.

  2. Все еще хорошая производительность. Все благодаря ленивой подгрузке ячеек.

Минусы:

  1. Отсутствие переиспользования ячеек. Без механизма переиспользования ячеек, который есть в UITableView, может снижаться производительность при обработке больших списков.

  2. Более требователен к ресурсам. Отсутствие переиспользования может привести к большей нагрузке на память и процессор, особенно при больших списках.

Безусловно были попытки адаптировать List: убрать стили таблиц, разделители, автоматический перевод символов хедера в верхний регистр, — но это оказалось практически невозможно на минимально поддерживаемой нами версии iOS. Для чего‑то нужно было отключить часть функционала у всех UITableView в приложении. Это требовало бы контроля за тем, чтобы не сломать отдельные экраны, и вряд ли прошло бы пулл реквест. В результате выбор пал на LazyVStack + ScrollView, потому что было важно соблюсти дизайн.

Думаем дальше, как отыскать способ улучшить перформанс. Настала пора воспользоваться профайлером.

Используем Xcode Profiler

Xcode Profiler — это ваш верный спутник в мире оптимизации и поиска узких мест в iOS приложениях. С его помощью можно как на ладони увидеть, где именно приложение тратит слишком много ресурсов или почему оно вдруг начало тормозить.

Используя Xcode Profiler, можно проводить всевозможные эксперименты: от мониторинга использования CPU до анализа расхода памяти. Это инструмент, который позволяет заглянуть «под капот» приложения и увидеть, как оно работает изнутри, найти утечки памяти, избыточное использование CPU и многое другое.

Пользоваться им просто: запускаем профайлер прямо из Xcode, выбираем нужный инструмент для анализа, и вперед — исследовать внутренности приложения. Там, в профайлере, можно найти графики, диаграммы, статистику — все, что нужно для детального анализа. Это как диагностика для автомобиля, только вместо автомобиля — приложение на iOS.

Жмем Command+I и видим такое окно:

Выбираем SwiftUI и запускаем приложение. Получаем такую картину.

Здесь можно посмотреть, какие вьюшки, как долго и в каком количестве рендерятся. Отсортировав по среднему времени отрисовки, можно найти проблемные и обратить на них пристальное внимание.

Я заметил, что дольше всех отрисовываются вьюшки, которые имеют внутри себя компоненты дизайн‑системы, обернутые в SwiftUI через UIViewRepresentable. Вывод напрашивается сам собой: нужно переписать компоненты дизайн‑системы на SwiftUI.

Это заняло какое‑то время, однако стоило того — производительность заметно выросла. Основная проблема с производительностью была решена, но хотелось еще чего‑нибудь оптимизировать.

Что еще можно оптимизировать

SwiftUI хорош в работе со стейтами, но иногда ему нужно помогать и подсказывать. Итак, что же можно сделать.

Используйте **.id**для SwiftUI View, чтобы указать уникальность элемента. Благодаря .id SwiftUI может эффективно обновлять только те части интерфейса, которые нуждаются в изменении, минимизируя тем самым количество работы для рендеринг-движка. Это особенно важно при работе с большими и динамичными данными.

Будьте осторожны с .onAppear и .onDisappear. Код в них выполняется синхронно и блокирует отрисовку до окончания исполнения. Лучше использовать .taks, он доступен с iOS 15. Формат работы похож на .onAppear, но исполнение кода происходит асинхронно. Это полезно, когда, например, нужно отправить аналитику просмотра какого-то элемента.

Попробуйте .drawingGroup(). Это позволит вынести вычисления, ответственные за отрисовку, на GPU, что значительно увеличит производительность. Однако использовать его можно не везде: например, вместе с компонентами WebImage, частью фреймворка SDWebImage, вместо картинки отобразится пустое поле.

Если используете в своих вью вычисляемые значения, старайтесь их не усложнять. Или вовсе от этого отказаться. Дело в том, что вью часто перерисовываются — и вычисления будут происходить при каждой перерисовке. Сложное значение более требовательно к мощности смартфона, а это значит, меньше ресурсов останется на отрисовку, что приведет к потере кадров.

Конечно, стоит избегать сложных вычислений и на главном потоке, в частности, при отрисовке вью.

Если следовать этим советам, у вас получится избежать существенной части проблем с производительностью на SwiftUI.

Проблема с кнопками при скролле

Когда вопрос производительности был решен и первые тестеры смогли потрогать новый экран, они постоянно жаловались, что экран иногда не скроллится. Оказалось, что проблема появляется, когда скролл начинается с места, где расположена кнопка «В корзину». Поначалу это даже казалось логичным, однако на UIKit‑версии такой проблемы не было. Пришлось разбираться.

После продолжительного расследования выяснилось, что кнопки из дизайн‑системы, которые я недавно переписал на SwiftUI, используют жесты: onTapGesture — чтобы триггернуть нажатие, а onLongPressGesture — чтобы реализовать эффект нажатия. Закомментировав эти части кода, я обнаружил, что скролл снова работает. Но нам‑то нужен полноценный рабочий скролл и красивые кнопки. Так что разбираемся дальше.

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

Решением стало написание и использование собственного ButtonStyle для кнопки, вдохновленное вовремя найденной статьей Даниэля Саиди.

Если коротко, то вот как это было.

@State private var isPressed = false

Button {
        onTap?()
    } label: {
        HStack {
            // appearance code
        }
        .scaleEffect(isPressed ? 0.96 : 1.0)
    }
    .buttonStyle(
        ScrollViewGestureButtonStyle(
            pressAction: {
                isPressed = true
            }, endAction: {
                isPressed = false
            }
        )
    )

А упрощенная версия ScrollViewGestureButtonStyle получилась такой. В версии Даниэля, на мой взгляд, было много лишнего. Я оставил только то, что нужно. 

//
//  ScrollViewGestureButtonStyle.swift
//
//  Created by Alexey Nepomnyashchikh on 28.11.2023.
//
//  Custom button style for SwiftUI buttons within a ScrollView.
//  Allows for complex gesture handling without blocking scroll gestures.
//  More information: https://danielsaidi.com/blog/2022/11/16/using-complex-gestures-in-a-scroll-view

import SwiftUI

/// A custom `ButtonStyle` for handling various gestures within a `ScrollView`.
/// This style allows for pressing, double-tapping, and long-pressing a button without interfering with the scroll view's gestures.
public struct ScrollViewGestureButtonStyle: ButtonStyle {

    private var doubleTapTimeout: TimeInterval
    private var longPressTime: TimeInterval

    private var pressAction: () -> Void
    private var longPressAction: () -> Void
    private var doubleTapAction: () -> Void
    private var endAction: () -> Void

    @State private var doubleTapDate = Date()
    @State private var longPressDate = Date()

    /// Initializes a new style with various gesture handlers.
    ///
    /// - Parameters:
    ///   - pressAction: A closure to execute when the button is pressed.
    ///   - doubleTapTimeout: The maximum time interval between double taps.
    ///   - doubleTapAction: A closure to execute on a double tap.
    ///   - longPressTime: The minimum duration for a long press.
    ///   - longPressAction: A closure to execute on a long press.
    ///   - endAction: A closure to execute when the gesture ends.
    public init(
        pressAction: @escaping (() -> Void) = {},
        doubleTapTimeout: TimeInterval = 0.5,
        doubleTapAction: @escaping () -> Void = {},
        longPressTime: TimeInterval = 1.0,
        longPressAction: @escaping () -> Void = {},
        endAction: @escaping () -> Void = {}
    ) {
        self.pressAction = pressAction
        self.doubleTapTimeout = doubleTapTimeout
        self.doubleTapAction = doubleTapAction
        self.longPressTime = longPressTime
        self.longPressAction = longPressAction
        self.endAction = endAction
    }

    public func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: configuration.isPressed) { isPressed in
                longPressDate = Date()
                if isPressed {
                    pressAction()
                    doubleTapDate = tryTriggerDoubleTap() ? .distantPast : Date()
                    tryTriggerLongPressAfterDelay(triggered: longPressDate)
                } else {
                    endAction()
                }
            }
    }

    /// Attempts to trigger a double tap action if the time interval between taps is within the specified timeout.
    private func tryTriggerDoubleTap() -> Bool {
        let interval = Date().timeIntervalSince(doubleTapDate)
        guard interval < doubleTapTimeout else { return false }
        doubleTapAction()
        return true
    }

    /// Attempts to trigger a long press action after a specified delay.
    private func tryTriggerLongPressAfterDelay(triggered date: Date) {
        DispatchQueue.main.asyncAfter(deadline: .now() + longPressTime) {
            guard date == longPressDate else { return }
            longPressAction()
        }
    }
}

Проблема решена. Теперь мы имеем красивые кнопки и рабочий скролл. 

Бонус

В SwiftUI много функционала, который работает на поздних версиях, а на ранних того же эффекта приходится добиваться по другому. В коде ниже я делюсь способом, где и как сделать if #available(iOS 16.0, *) внутри SwiftUI View, а заодно и некоторыми фишками, которые мне пригодились.

//
//  SwiftUI+Backport.swift
//
//  Created by Alexey Nepomnyashchikh on 26.10.2023.
//

import Foundation
import SwiftUI

/// A wrapper struct `Backport` that is used to backport SwiftUI features not available in earlier versions of iOS.
/// It provides a consistent way to handle conditional SwiftUI view modifications based on the iOS version.
public struct Backport<Content> {
    /// The content this `Backport` wraps. It can be any type of SwiftUI View.
    public let content: Content

    /// Initializes a new `Backport` with the provided content.
    /// - Parameter content: The SwiftUI view to be wrapped for backporting features.
    public init(_ content: Content) {
        self.content = content
    }
}

extension View {
    /// A computed property that wraps the SwiftUI View in a `Backport` struct.
    /// This enables calling backport-specific modifiers on any SwiftUI View.
    var backport: Backport<Self> { Backport(self) }
}

extension Backport where Content == Image {
    /// Provides a backported version of the `tint` modifier for `Image`.
    /// In iOS 16 and later, it uses the standard `tint` modifier.
    /// In earlier versions, it manually sets the rendering mode and foreground color to simulate tinting.
    /// - Parameter color: The color to use for tinting the image.
    /// - Returns: A view (Image) that is either tinted using the iOS 16 `tint` feature or manually in earlier versions.
    @ViewBuilder func tint(_ color: Color) -> some View {
        if #available(iOS 16.0, *) {
            content.tint(color)
        } else {
            content
                .renderingMode(.template)
                .foregroundColor(color)
        }
    }
}

extension Backport where Content: View {
    /// Provides a backported version of the `.task` modifier.
    /// In iOS 15.0 and later, it uses the standard `.task` modifier.
    /// In earlier versions, it manually handles asynchronous execution.
    /// - Parameter action: The asynchronous action to perform.
    @ViewBuilder func doAsyncTask(_ action: @escaping () -> Void) -> some View {
        if #available(iOS 15.0, *) {
            content.task {
                action()
            }
        } else {
            content.onAppear {
                DispatchQueue.global(qos: .userInitiated).async {
                    action()
                }
            }
        }
    }
}

extension Backport where Content: View {

    /// Applies a monospaced digit style to the content.
    ///
    /// This method backports the `monospacedDigit` modifier functionality for views.
    /// On iOS 15 and later, it uses the standard `monospacedDigit` modifier.
    /// On earlier versions of iOS, this method can optionally apply a system monospaced font.
    ///
    /// - Parameter force: A Boolean value that determines whether to force the application of a monospaced font
    ///                    on versions of iOS prior to 15.0. If `true`, a system monospaced font is applied.
    ///                    If `false`, the content remains unmodified. The default value is `false`.
    ///
    /// - Returns: A view that is modified with the `monospacedDigit` modifier on iOS 15.0 and later,
    ///            or with a monospaced system font on earlier versions if `force` is `true`.
    @ViewBuilder func monospacedDigit(force: Bool = false) -> some View {
        if #available(iOS 15.0, *) {
            content.monospacedDigit()
        } else {
            if force {
                content.font(.system(size: UIFont.systemFontSize, weight: .regular, design: .monospaced))
            } else {
                content
            }
        }
    }
}

extension Backport where Content: View {
    /// Provides a backported version of the `backgroud` modifier for `View`
    /// In iOS 15 and later, it uses new
    /// `background<S, T>(_ style: S, in shape: T, fillStyle: FillStyle = FillStyle())`
    ///  modifier to set clipped backgroud
    /// In earlier versions, this can only be done with a combination of modifiers `backgroud` and `clipShape`
    /// - Parameter color: The color to use for background.
    /// - Parameter shape: The color to use for clipping the view.
    /// - Returns: A view with backgroud and shape installed
    @ViewBuilder func backgroud<T>(_ color: Color, in shape: T) -> some View where T: InsettableShape {
        if #available(iOS 15.0, *) {
            content.background(color, in: shape)
        } else {
            content
                .background(color)
                .clipShape(shape)
        }
    }
}

Выводы

Несмотря на трудности, с которыми пришлось столкнуться, я оцениваю опыт использования SwiftUI как положительный. Набив определенное количество шишек, переписав важные компоненты дизайн системы и сформировав подход к использованию SwiftUI удалось популяризировать SwiftUI внутри команд разработки и теперь в новых фичах чаще и чаще используется SwiftUI. Когда процесс работы со SwiftUI налажен разработка ведется гораздо быстрее и приятнее. Учитывая тот факт, что наше приложение для Android использует Compose, использование SwiftUI в iOS сблизило приложения для разных платформ и упростило взаимную интеграцию разработчиков в новые платформы за счет более знакомого механизма верстки.

Советы

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

Большая часть проблем со SwiftUI отпадает начиная с iOS 15. Поэтому рекомендую поднять таргет как минимум до iOS 15. Уже после релиза фичи обнаружились плавающие краши, связанные со SwiftUI, которые встречаются только на iOS 14. Решение здесь только одно — повышать таргет.

Начните с переписывания компонентов дизайн системы на SwiftUI, хотя бы самых распространенных.

Дерзайте!

Комментарии (0)