Один из лучших способов освоить SwiftUI (как и большинство технологий) — это реализовать что-то небольшое и полезное. Всплывающее окно (поп-ап) является отличным вариантом — это один из самых популярных инструментов интерфейса, который часто нужно кастомизировать, если нативный вариант не устраивает по виду или функционалу.
В этом туториале мы создадим минимальный вариант всплывающего окна на SwiftUI с нуля. В итоге получим рабочий компонент пользовательского интерфейса, который можно внедрить в свое SwiftUI приложение или продолжить его настраивать и улучшать. Вот как это будет выглядеть:

Выбор API
Сначала определимся с функциональностью поп-апа, который мы хотим реализовать. Это будет элемент, который отображается при нажатии на кнопку. В нем будет отображен просто лейбл. Он будет появляться и исчезать анимированно. Нам нужна возможность определять внешний вид и стиль поп-апа, добавлять его в стек интерфейса и контролировать его состояние.
Мы начнем с написания примера использования поп-апа, как будто он уже существует, а самой реализацией займёмся чуть позже. Такой подход позволит сразу понять, как будет удобнее использовать компонент в своем коде.
struct ContentView : View { @State var showingPopup = false // 1 var body: some View { ZStack { Color.red.opacity(0.2) Button("Push me") { showingPopup = true // 2 } } .popup(isPresented: $showingPopup) { // 3 ZStack { // 4 Color.blue.frame(width: 200, height: 100) Text("Popup!") } } } }
Вот что делает этот простой пример:
-
Мы добавляем всплывающее окно в качестве модификатора нашего элемента, передавая @Binding showingPopup для управления состоянием внутри реализации самого элемента.Переменная @Statevar showingPopup будет управлять отображением поп-апа.
-
Отдельная кнопка на экране будет изменять состояние переменной showingPopup.
-
Мы добавляем всплывающее окно в качестве модификатора нашего элемента, передавая @BindingshowingPopup для управления состоянием внутри реализации самого элемента.
-
Дизайн и содержимое всплывающего окна также передаются в качестве параметра.
-
Сам поп-ап реализован как ViewModifier, как принято в SwiftUI.
Теперь, когда у нас есть представление о желаемом интерфейсе и внешнем виде поп-апа, давайте приступим к фактической реализации.
Реализация View Modifier
extension View { public func popup<PopupContent: View>( isPresented: Binding<Bool>, view: @escaping () -> PopupContent) -> some View { self.modifier( Popup( isPresented: isPresented, view: view) ) } }
Этот фрагмент кода не требует пояснений — это определение модификатора поп-апа для View. Мы уже знаем, что нам нужны два параметра — isPresented, который представляет собой Binding Property Wrapper для управления состоянием всплывающего окна, и view, который отвечает за внешний вид поп-апа.
Теперь мы можем приступить к самой интересной части.
Реализация всплывающего элемента
Инициализатор и публичные переменные понятны — они уже были определены и объяснены, когда мы выбирали API:
public struct Popup<PopupContent>: ViewModifier where PopupContent: View { init(isPresented: Binding<Bool>, view: @escaping () -> PopupContent) { self._isPresented = isPresented self.view = view } /// Controls if the sheet should be presented or not @Binding var isPresented: Bool /// The content to present var view: () -> PopupContent
Список приватных переменных расскажет больше о деталях реализации — поп-ап будет отображаться и скрываться путем изменения оффсета view, которое его содержит. Этот оффсет легко посчитать и анимировать, и он подходит для всех размеров экрана, поскольку прямым образом от него зависит.
Приватные переменные будут содержать данные фрейма, необходимые для показа и скрытия поп-апа — фреймы родительского контроллера и содержимого поп-апа, оффсеты для скрытого и показанного состояния, а также вспомогательные методы для получения размера экрана.
// MARK: - Private Properties /// The rect of the hosting controller @State private var presenterContentRect: CGRect = .zero /// The rect of popup content @State private var sheetContentRect: CGRect = .zero /// The offset when the popup is displayed private var displayedOffset: CGFloat { -presenterContentRect.midY + screenHeight/2 } /// The offset when the popup is hidden private var hiddenOffset: CGFloat { if presenterContentRect.isEmpty { return 1000 } return screenHeight - presenterContentRect.midY + sheetContentRect.height/2 + 5 } /// The current offset, based on the "presented" property private var currentOffset: CGFloat { return isPresented ? displayedOffset : hiddenOffset } private var screenWidth: CGFloat { UIScreen.main.bounds.size.width } private var screenHeight: CGFloat { UIScreen.main.bounds.size.height }
Само UI-наполнение поп-апа минимально: мы считываем фрейм основного контента, затем добавляем sheet оверлея, содержащий поп-ап. Сам sheet делает практически то же самое — считывает свой фрейм для позиционирования видимого UI поп-апа, а также добавляет обработчик для удаления показанного поп-апа по нажатию и простую анимацию. Тут же используется ранее вычисленный currentOffset:
// MARK: - Content Builders public func body(content: Content) -> some View { ZStack { content .frameGetter($presenterContentRect) } .overlay(sheet()) } func sheet() -> some View { ZStack { self.view() .simultaneousGesture( TapGesture().onEnded { dismiss() }) .frameGetter($sheetContentRect) .frame(width: screenWidth) .offset(x: 0, y: currentOffset) .animation(Animation.easeOut(duration: 0.3), value: currentOffset) } } private func dismiss() { isPresented = false }
Чтобы реализовать простую анимацию в SwiftUI, достаточно добавить однострочный модификатор, что мы и делаем в конце создания sheet. Естественно, его можно анимировать как вам угодно — просто заменив итоговый размер и положение на экране, можно получить другой тип UI элемента (например, верхний или нижний тост).
Скорее всего, вы обратили внимание на модификатор frameGetter. Это некрасивый, но необходимый в SwiftUI метод получения фрейма (по крайней мере, судя по документации, более удобного способа получить фрейм в SwiftUI у нас нет). Надеемся, что в будущем появится более удобный способ:
extension View { func frameGetter(_ frame: Binding<CGRect>) -> some View { modifier(FrameGetter(frame: frame)) } } struct FrameGetter: ViewModifier { @Binding var frame: CGRect func body(content: Content) -> some View { content .background( GeometryReader { proxy -> AnyView in let rect = proxy.frame(in: .global) // This avoids an infinite layout loop if rect.integral != self.frame.integral { DispatchQueue.main.async { self.frame = rect } } return AnyView(EmptyView()) }) } }
GeometryReader — это view в SwiftUI, который в качестве параметра принимает координатное пространство и предоставляет информацию о своем размере содержимому (иными словами, как раз то, что нужно для получения данных о фрейме).
Заключение
Приведенный выше код — это все, что нам нужно для простой версии поп-апа, и большая его часть понятна и легко читается. Этот код может стать основой для более сложной реализации всплывающего окна, если вам нужно что-то более кастомное.
Можете также посмотреть на нашу библиотеку для поп-апа на SwiftUI. На самом деле, этот туториал — упрощенная версия кода в нашем репозитории. Он заметно более сложен из-за большего количества стилей для поп-апов, расширенных параметров и коллбэков, а также поддержкой нескольких платформ. Сам код отображения и добавления поп-апа идентичен приведённому выше. Библиотека предоставляет как простые поп-апы:

так и более сложные кастомные вью:

ссылка на оригинал статьи https://habr.com/ru/post/647247/
Добавить комментарий