iOS 18 для разработчиков: Ключевые изменения в UIKit

3bb3f3db273798311abd755629c3f555.png

Привет! Меня зовут Лена, я работаю iOS-разработчиком в KTS. Недавно вышла новая версия iOS 18, и я решила подробно изучить все нововведения, чтобы понять, какие новые возможности она предлагает разработчикам. В этой статье расскажу и покажу самые интересные обновления в UIKit — новый TabBar, анимации, совместимость UIKit/SwiftUI и многое другое.

Оглавление

Основные изменения

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

Сравнение кода в iOS 17 и iOS 18

Рассмотрим, как настраивался контроллер для работы с документами в предыдущей версии iOS 17:

class DocumentViewController: UIDocumentViewController { ... }
let documentViewController = DocumentViewController()
let browserViewController = UIDocumentBrowserViewController(
    forOpening: [.plainText]
)
window.rootViewController = browserViewController
browserViewController.delegate = self
// MARK: UIDocumentBrowserViewControllerDelegate
func documentBrowser(
    _ browser: UIDocumentBrowserViewController, 
    didPickDocumentsAt documentURLs: [URL]
) {
    guard let url = documentURLs.first else { return }
    documentViewController.document = StoryDocument(fileURL: url)
    browser.present(documentViewController, animated: true)
}

В iOS 17 для настройки интерфейса просмотра файлов требовалось создавать два контроллера: UIDocumentBrowserViewController для работы с файлами и пользовательский DocumentViewController для отображения выбранного документа. Также нужно было реализовать делегат UIDocumentBrowserViewControllerDelegate для обработки выбора файлов.

В новой версии iOS 18 разработчики получили возможность значительно упростить код:

class DocumentViewController: UIDocumentViewController { ... }
let documentViewController = DocumentViewController()
window.rootViewController = documentViewController

Теперь достаточно одного контроллера — DocumentViewController, который сразу назначается корневым контроллером окна. Это нововведение позволяет отказаться от использования UIDocumentBrowserViewController и делегатных методов для открытия файлов, делая код более компактным и читаемым.

Результат

Результат

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

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

    class DocumentViewController: UIDocumentViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Update the background
        launchOptions.background.image = UIImage(resource: .fileBackground)
        // Add foreground accessories
        launchOptions.foregroundAccessoryView = ForegroundAccessoryView()
    }
}

Новое свойство launchOptions значительно упрощает процесс кастомизации интерфейса.

Кастомизация интерфейса

Кастомизация интерфейса

Новый TabBar в iPadOS 18

С выходом iPadOS 18 появился новый удобный API для создания панели вкладок (TabBar), который значительно упрощает навигацию и управление интерфейсом. Теперь панель вкладок располагается в верхней части экрана, что упрощает навигацию. Вкладки расположены ближе друг к другу, что улучшает взаимодействие и упрощает переход между разделами.

Кроме того, новый интерфейс предоставляет гибкость в настройке вида панели — можно легко переключаться между tabBar и sideBar, адаптируя дизайн под требования приложения.

Чтобы добавить вкладки, разработчики могут использовать новый подход, который позволяет установить массив вкладок (UITab) в свойство tabs контроллера.

Пример кода

class TabBarController: UITabBarController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let favoritesSection = UITabGroup(
            title: "Favorites",
            image: UIImage(systemName: "heart.circle"),
            identifier: "Section Favorites",
            children:
                [
                    UITab(title: "My Books",
                          image: UIImage(systemName: "heart.circle"),
                          identifier: "My Books Tab") { _ in
                              UIViewController()
                          },
                    
                    UITab(title: "My Films",
                          image: UIImage(systemName: "heart.circle"),
                          identifier: "My Films Tab") { _ in
                              UIViewController()
                          },
                    
                    UITab(title: "My Music",
                          image: UIImage(systemName: "heart.circle"),
                          identifier: "My Music Tab") { _ in
                              UIViewController()
                          }
                ]) { _ in
                    UIViewController()
                }
        
        self.tabs = [
            UITab(title: "Books",
                  image: UIImage(systemName: "book"),
                  identifier: "Books Tab") { _ in
                      UIViewController()
                  },
            
            UITab(title: "Films",
                  image: UIImage(systemName: "film.stack"),
                  identifier: "Films Tab") { _ in
                      UIViewController()
                  },
            
            UITab(title: "Music",
                  image: UIImage(systemName: "music.quarternote.3"),
                  identifier: "Music Tab") { _ in
                      UIViewController()
                  },
            favoritesSection,
            
            UISearchTab { _ in
                UINavigationController(
                    rootViewController: UISearchController()
                )
            }
        ]
    }
}

Здесь также добавлена отдельная секция UITabGroup с вкладками. При добавление хотя бы одной группы вкладок (UITabGroup) автоматически включает поддержку боковой панели (sideBar). У пользователя есть возможность самому менять вид, а sideBar автоматически появляется в landscape ориентации:

UITabGroup с вкладками

UITabGroup с вкладками

sideBar

sideBar

Для создания вкладок достаточно установить массив UITab в свойство tabs контроллера, добавляя как обычные вкладки, так и группы вкладок (UITabGroup).

Независимо от структуры вкладок, в iPadOS 18 можно управлять видом панели через свойства:

        // Enable the sidebar.
        self.mode = .tabSidebar

        // Get the sidebar.
        let sidebar = self.sidebar

        // Show the sidebar.
        self.sidebar.isHidden = false

Новый API поддерживает адаптацию для работы на Mac Catalyst и VisionOS, что делает его универсальным для разных платформ.

Плавные переходы (fluid transitions)

В iOS 18 представлен новый интерактивный переход (transition) с масштабированием. Этот переход позволяет захватывать и перетаскивать view как в начале, так и в процессе перехода, обеспечивая более гибкое и плавное взаимодействие с пользовательским интерфейсом.

SwiftUI/UIKit interoperability

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

Рассмотрим обновления в двух областях: анимации и распознавателях жестов.

Animations

Теперь в iOS 18 можно применять типы анимации SwiftUI для анимации UIView, что открывает доступ ко всему набору анимаций SwiftUI, включая CustomAnimations. Это позволяет создавать плавные и выразительные анимации с меньшими усилиями.

Примеры использования:

SwiftUI

        withAnimation(.spring(duration: 0.5)) {
            beads.append(Bead())
        }   

UIKit с анимациями SwiftUI

        UIView.animate(.spring(duration: 0.5)) {
            bead.center = endOfBracelet
        }

Также стало проще создавать плавные анимации, управляемые жестами, используя spring-анимации SwiftUI. Подробности можно найти в видео Enhance your UI animations and transitions.

Кроме того, можно использовать SwiftUI анимации для создания анимаций в UIKit, управляемой жестами:

switch gesture.state {
case .changed:
    UIView.animate(.interactiveSpring) {
        bead.center = gesture.translation
    }
case .ended:
    UIView.animate(.spring) {
        bead.center = endOfBracelet
    }
}

Gesture recognizers 

В iOS 18 распознаватели жестов в SwiftUI и UIKit были унифицированы, что позволяет задавать зависимости между жестами разных фреймворков. Это упрощает управление взаимодействием, когда SwiftUI представление встроено в UIKit и наоборот.

Например, можно сделать так, чтобы UITapGestureRecognizer в UIKit зависел от завершения TapGesture в SwiftUI, предотвращая конфликт жестов. Такая унификация позволяет создавать более гибкие и отзывчивые интерфейсы.

UIKit зависит от завершения TapGesture в SwiftUI

UIKit зависит от завершения TapGesture в SwiftUI

По умолчанию в iOS 18 жесты распознаются одновременно, и при двойном клике могут вызываться оба обработчика.

Если указать имя жесту SwiftUI, например, "SwiftUIDoubleTap", это можно использовать в делегате UIGestureRecognizerDelegate для управления приоритетом жестов. В данном случае, жест UIKit (tapGesture) будет выполняться только если жест SwiftUI с именем "SwiftUIDoubleTap" завершился с ошибкой.

class ViewController: UIViewController, UIGestureRecognizerDelegate {
    func gestureRecognizer(
        _ gestureRecognizer: UIGestureRecognizer,
        shouldRequireFailureOf other: UIGestureRecognizer
    ) -> Bool {
        return other.name == "SwiftUIDoubleTap"
    }
}
struct GestureView: View {
    let doubleTap = TapGesture(count: 2)
    var body: some View {
        Circle()
            .gesture(doubleTap, name: "SwiftUIDoubleTap")
    }
}

Общие улучшения UIKit

Automatic trait tracking

В iOS 18 UIKit добавил поддержку автоматического отслеживания traitCollection в методах обновления UIView и UIViewController представлений, таких как layoutSubviews и drawRect. Когда система вызывает один из этих методов, она автоматически фиксирует, к каким признакам (traits) вы обращаетесь внутри метода. 

Если одно из отслеживаемых свойств изменяется, UIKit автоматически вызывает соответствующий метод для обновления, например, setNeedsLayout или setNeedsDisplay, чтобы обеспечить корректное обновление интерфейса.

В iOS 17 для обновления интерфейса при изменении UITraitHorizontalSizeClass необходимо было вручную отслеживать изменения и вызывать setNeedsLayout. Это требовало дополнительной проверки в коде, чтобы определить, произошли ли изменения в horizontalSizeClass, и соответственно обновить макет.

Так бы выглядел код на iOS 17, где необходимо отслеживать признак orizontalSizeClass, чтобы соответственно изменить верстку:

Код на iOS 17

class MyView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        registerForTraitChanges(
            [UITraitHorizontalSizeClass.self],
            action: #selector(UIView.setNeedsLayout)
        )
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        if traitCollection.horizontalSizeClass == .compact {
            setCompactLayout()
        } else {
            setRegularLayout()
        }
    }
}

С появлением автоматического отслеживания признаков (automatic trait tracking) в iOS 18, необходимость вручную регистрировать изменения исчезла. Теперь, когда вызывается метод layoutSubviews, система автоматически регистрирует использование признака horizontalSizeClass. Если этот признак изменяется, UIKit сам вызывает соответствующий метод обновления, например, setNeedsLayout.

class MyView: UIView {
    override func layoutSubviews() {
        super.layoutSubviews()
        if traitCollection.horizontalSizeClass == .compact {
            setCompactLayout()
        } else {
            setRegularLayout()
        }
    }
}

List environment trait

В iOS 18 обновлены API коллекций и табличных представлений, что упрощает процесс обновления ячеек. Все представления в секциях списков UICollectionView и UITableView теперь имеют свойство traitCollection.listEnvironment, которое описывает стиль списка.

С введением listEnvironment в iOS 18, описывающим стиль списка, объекты UIListContentConfiguration и UIBackgroundConfiguration стали использовать эту новую особенность.

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

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

Например, рассмотрим вкладку «Обзор» в приложении «Файлы».

Вкладка «Обзор» в приложении «Файлы»

Вкладка «Обзор» в приложении «Файлы»

В этом примере используется UICollectionView с композиционным макетом для отображения списка местоположений, избранного и тегов в двух разных стилях, в зависимости от контекста:

  • Книжная ориентация: В этом случае список отображается с использованием стиля insetGrouped, что создает аккуратное и организованное представление;

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

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

Боковая панель

Боковая панель

Рассмотрим функцию из приложения «Файлы» в iOS 17, которая генерирует конфигурации содержимого и фона для ячеек местоположений в списке просмотра:

func configurations(for location: FileLocation,
                        listAppearance: UICollectionLayoutListConfiguration.Appearance) ->
    (UIListContentConfiguration, UIBackgroundConfiguration) {

        let isSideBar = listAppearance == .sidebar

        var contentConfiguration: UIListContentConfiguration
        let backgroundConfiguration: UIBackgroundConfiguration

        contentConfiguration = isSideBar ? .sidebarCell() : .cell()

        backgroundConfiguration = isSideBar ? .listSidebarCell() : .listGroupedCell()

        contentConfiguration.text = location.title
        contentConfiguration.image = location.thumbnailImage
       
        return (contentConfiguration, backgroundConfiguration)
    }

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

Функция принимает в качестве аргументов структуру FileLocation и внешний вид списка (listAppearance). Сначала она проверяет, соответствует ли внешний вид списка стилю боковой панели, и сохраняет результат в локальной переменной isSideBar.

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

func configurations(for location: FileLocation) ->
    (UIListContentConfiguration, UIBackgroundConfiguration) {

    var contentConfiguration = UIListContentConfiguration.cell()
    let backgroundConfiguration = UIBackgroundConfiguration.listCell()

    contentConfiguration.text = location.title
    contentConfiguration.image = location.thumbnailImage

    return (contentConfiguration, backgroundConfiguration)
}

В iOS 18 функцию конфигурации ячеек можно значительно упростить. Приложение «Файлы» теперь использует новый конструктор ячеек для настройки содержимого и конструктор listCell для фоновой конфигурации.

Когда конфигурации применяются к ячейке, они автоматически обновляются в зависимости от состояния конфигурации ячейки. Благодаря новому listEnvironment, эти конфигурации теперь синхронизируют свои свойства со стилем списка. Для UIListContentConfiguration и UIBackgroundConfiguration существующие конфигурации ячеек обновляют свой внешний вид на основе признака среды списка. Для UIBackgroundConfiguration добавлены три новых конструктора: listCell, listHeader и listFooter.

UIUpdateLink

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

  • Автоматическое отслеживание представлений: UIUpdateLink автоматически подстраивается под родительское представление, что упрощает управление анимациями.

  • Режим с низкой задержкой: Эта функция позволяет перевести систему в режим с низкой задержкой, что особенно полезно для приложений рисования.

Кроме того, UIUpdateLink способствует более высокой производительности и эффективному использованию батареи благодаря своим расширенным функциям.

Пример imageView, анимируемого вверх и вниз с помощью функции синуса:  

class UpdateLinkViewController: UIViewController {
    lazy var updateLink = UIUpdateLink(
        view: imageView,
        actionTarget: self,
        selector: #selector(update)
    )
    override func viewDidLoad() {
        super.viewDidLoad()
        updateLink.requiresContinuousUpdates = true
        updateLink.isEnabled = true
    }
    @objc func update(updateLink: UIUpdateLink,
                      updateInfo: UIUpdateInfo) {
              imageView.center.y = sin(updateInfo.modelTime)
            * 100 + view.bounds.midY
    }
}

При инициализации UIUpdateLink требуется указать экземпляр UIView. Он автоматически активируется, когда представление становится видимым на экране, и отключается, когда представление скрывается.

В функции обновления используется параметр updateInfo.modelTime для отслеживания времени последнего обновления. Установка свойства requireContinuousUpdates в true обеспечивает постоянные обновления, пока UIUpdateLink активен. При значении false обновления происходят только при наличии внешних триггеров, таких как жесты или изменения слоя.

Symbol animations

В iOS 18 SF Symbols в UIKit получили новые возможности для анимации символов, включая три пресета анимации:

  • .wiggle — заставляет символ колебаться в любом направлении или под любым углом, чтобы привлечь внимание пользователя;

  • .breathe — плавно масштабирует символ, создавая эффект «дыхания»;

  • .rotate — вращает часть символа вокруг назначенной точки привязки, добавляя динамичности.

Также добавлено новое поведение .periodic, которое позволяет задавать количество повторений анимации и настраивать задержку между ними.

Кроме того, эффект .replace теперь использует «магическую замену» по умолчанию, плавно анимируя смену значков и отображение косых черт, делая переходы более естественными.

Чтобы добавить эффект:

imageView.addSymbolEffect(.rotate)

Эффект replace:

let imageView = UIImageView(image: UIImage(systemName: "person.crop.circle.badge.clock"))
            imageView.setSymbolImage(UIImage(systemName: "person.crop.circle.badge.checkmark")!, contentTransition: .replace)

Sensory feedback

В iPadOS 17.5 улучшена сенсорная обратная связь для iPad с использованием Apple Pencil Pro и Magic Keyboard. UIFeedbackGenerator теперь поддерживает новые способы предоставления тактильной обратной связи и может быть привязан к представлению как взаимодействие.

Ключевые обновления:

  • Указание местоположения: При предоставлении обратной связи приложениям теперь нужно указывать координаты действия внутри представления (location), которое вызвало обратную связь.

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

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

@ViewLoading var feedbackGenerator: UICanvasFeedbackGenerator
override func viewDidLoad() {
    super.viewDidLoad()
    feedbackGenerator = UICanvasFeedbackGenerator(view: view)
}
func dragAligned(_ sender: UIPanGestureRecognizer) {
    feedbackGenerator.alignmentOccurred(at: sender.location(in: view))
}

При создании feedbackGenerator его необходимо связать с представлением (view). Когда срабатывает обратная связь, передается местоположение, инициировавшее действие, например, координаты распознавателя жестов, который активировал выравнивание фигуры.

Теперь, если для перетаскивания используется Apple Pencil Pro, тактильная обратная связь обеспечивается прямо через стилус, когда фигура перемещается по направляющей привязки.

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

Text improvenments

В iOS 18 расширены возможности форматирования текста: в UITextView добавлена встроенная панель инструментов для редактирования. Она позволяет менять шрифт, размер и цвет текста, а также добавлять атрибуты, такие как списки, что упрощает процесс форматирования и делает его более удобным для пользователя. 

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

Выделение работает путем применения двух новых атрибутов. textHighlightStyle — для диапазона текста, который должен быть выделен, и textHighlightColorScheme — для цвета, используемого для визуализации выделения.

var attributes = [NSAttributedString.Key: Any]()   
        // Highlight style
        attributes[.textHighlightStyle] = NSAttributedString.TextHighlightStyle.default

        // Highlight color scheme
        attributes[.textHighlightColorScheme] = NSAttributedString.TextHighlightColorScheme.orange       

Кроме стандартной цветовой схемы с использованием оттенка, в iOS 18 представлено пять предустановленных цветовых схем. Эти схемы позволяют быстро применять разные цветовые стили для текстового форматирования, расширяя возможности кастомизации.

Панель форматирования текста можно настроить путем установки UITextFormattingViewController.Configuration в textView:

Панель форматирования текста

Панель форматирования текста

Writing Tools support

Writing Tools — новая функция в iOS, iPadOS и macOS, которая расширяет возможности редактирования текста, предлагая инструменты для корректировки и преобразования текста. Панель Writing Tools появляется поверх клавиатуры при выделении текста, а также в контекстном меню рядом с опциями «Вырезать», «Копировать» и «Вставить».

Панель Writing Tools

Панель Writing Tools

По умолчанию UITextView, NSTextView и WKWebView поддерживают функцию Writing Tools, но для полноценной работы с ее возможностями необходимо использовать TextKit 2. Это обеспечивает более широкий набор функций для редактирования и обработки текста.

К примеру, с помощью Writing Tools можно преобразовать текст в таблицу:

Преобразование текста в таблицу

Преобразование текста в таблицу

Подробнее о Writing Tools можно посмотреть в докладе Get started with Writing Tools.

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

Наши другие статьи про разработку на iOS:

© Habrahabr.ru