Простой интерактивный поп-ап для iOS — туториал по SwiftUI

от автора

Один из лучших способов освоить 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!")             }         }     } }

Вот что делает этот простой пример:

  1. Мы добавляем всплывающее окно в качестве модификатора нашего элемента, передавая @Binding showingPopup для управления состоянием внутри реализации самого элемента.Переменная @Statevar showingPopup будет управлять отображением поп-апа.

  2. Отдельная кнопка на экране будет изменять состояние переменной showingPopup.

  3. Мы добавляем всплывающее окно в качестве модификатора нашего элемента, передавая @BindingshowingPopup для управления состоянием внутри реализации самого элемента.

  4. Дизайн и содержимое всплывающего окна также передаются в качестве параметра.

  5. Сам поп-ап реализован как 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/