Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров5.3K

Один из лучших способов освоить 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. На самом деле, этот туториал — упрощенная версия кода в нашем репозитории. Он заметно более сложен из-за большего количества стилей для поп-апов, расширенных параметров и коллбэков, а также поддержкой нескольких платформ. Сам код отображения и добавления поп-апа идентичен приведённому выше. Библиотека предоставляет как простые поп-апы:

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

Теги:
Хабы:
+4
Комментарии0

Публикации

Изменить настройки темы

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн