Форум программистов, компьютерный форум, киберфорум
mobDevWorks
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Свой попап в SwiftUI

Запись от mobDevWorks размещена 03.04.2025 в 16:23
Показов 1651 Комментарии 0
Метки ios, mobile, mobiledev, popup, swiftui

Нажмите на изображение для увеличения
Название: 9dba548f-d9e4-4260-981e-b51abe6967fc.jpg
Просмотров: 94
Размер:	61.6 Кб
ID:	10519
SwiftUI, как декларативный фреймворк от Apple, предоставляет множество инструментов для создания пользовательских интерфейсов. В нашем распоряжении есть такие API как alerts, popovers, action sheets и modal sheets. Но здесь кроется интересный парадокс – при всём богатстве выбора, фреймворк не даёт нам возможности создавать по-настоящему кастомные попапы или снэкбары. Если ваш дизайнер придумал что-то выходящее за рамки стандартных компонентов – готовьтесь к тому, что реализовать это будет непросто.

В UIKit ситуация выглядит иначе. Имея в своём распоряжении UIViewController и его методы презентации, разработчик получает больше контроля над процессом отображения модальных окон. Можно настроить анимации перехода, стили презентации и прочие параметры. Но и цена за это соответствующая – десятки строк императивного кода против нескольких декларативных конструкций в SwiftUI.

При переходе с UIKit на SwiftUI многие разработчики сталкиваются с вопросом: как перенести привычную гибкость в новую парадигму? Как создать свои собственные попапы, которые будут не только соответствовать дизайну приложения, но и сохранят плавность анимаций и интуитивность использования?

В этой статье мы шаг за шагом разберём процесс создания универсального попап-компонента в SwiftUI. Наша цель – разработать гибкое решение, которое можно будет применить в различных сценариях: от простых уведомлений до сложных диалогов с формами ввода. Мы рассмотрим архитектурные основы компонента, особенности его позиционирования на экране и различные варианты анимаций. Интересно, что подход к созданию кастомных попапов в SwiftUI основан на использовании базовых механизмов фреймворка – модификаторов представления и композиции. Это очень характерно для SwiftUI: создание сложных компонентов путём комбинирования простых, с минимальным количеством кода.

Попап, который мы создадим, будет работать как ViewModifier – особый тип объекта в SwiftUI, который может изменять или дополнять существующее представление. Этот подход позволит применять наш попап к любому представлению с помощью простого синтаксиса вроде .popup(isPresented: $showPopup) { /* содержимое попапа */ }.

Для тех, кто уже работал с UIKit, такой подход может показаться необычным. В UIKit мы привыкли думать о презентации как о взаимодействии между view-контроллерами. В SwiftUI же всё вращается вокруг представлений и их состояний. Эта разница в парадигмах требует некоторой перестройки мышления, но результат того стоит – гораздо более лаконичный и понятный код.

Архитектурные основы



Чтобы создать продуманный попап-компонент в SwiftUI, необходимо хорошо понимать, как устроена иерархия представлений в этом фреймворке. В отличие от UIKit, где мы имеем дело с деревом UIView, в SwiftUI всё основано на композиции структур, реализующих протокол View. Каждое представление описывает часть пользовательского интерфейса, а весь экран формируется за счёт вложения одних представлений в другие. Именно эта особенность архитектуры SwiftUI позволяет нам создавать попапы без сложных манипуляций с окнами или контроллерами. Но здесь же кроется и основная сложность – как разместить попап так, чтобы он появлялся поверх всего контента, но при этом оставался частью дерева представлений?

В SwiftUI существует несколько способов наложения контента. Когда речь заходит о попапах, на ум сразу приходят два подхода: использование ZStack или модификатора .overlay(). Давайте рассмотрим их подробнее.

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Подход с ZStack
ZStack {
    ContentView()
    if showPopup {
        PopupView()
    }
}
 
// Подход с overlay
ContentView()
    .overlay(
        Group {
            if showPopup {
                PopupView()
            }
        }
    )
На первый взгляд, оба варианта делают одно и то же – размещают PopupView поверх ContentView. Но в них есть тонкое различие, которое может оказаться критичным. При использовании ZStack мы явно определяем порядок отрисовки – сначала ContentView, затем PopupView. А вот .overlay() – это модификатор, который применяется к уже существующему представлению. Он добавляет наложение поверх модифицируемого представления, но при этом сохраняет все остальные его свойства. Это означает, что если к ContentView применяются другие модификаторы после .overlay(), они могут повлиять и на PopupView, что не всегда желательно. Тут вступает в игру ещё один важный аспект – размер попапа и его позиционирование. В SwiftUI размер представления определяется через взаимодействие предложения родителя и предпочтений дочернего элемента. Если мы хотим, чтобы наш попап занимал всё доступное пространство (для фона с затемнением) или позиционировался по центру, нам понадобится GeometryReader:

Swift Скопировано
1
2
3
4
5
6
7
8
9
ContentView()
    .overlay(
        GeometryReader { geometry in
            if showPopup {
                PopupView()
                    .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
    )
GeometryReader жадно захватывает всё доступное пространство, предоставляя нам информацию о размерах через параметр geometry. Это позволяет точно контролировать размеры и положение попапа.

Теперь поговорим о принципах композиции при проектировании переиспользуемых попап-компонентов. Ключевая идея SwiftUI – создание сложных интерфейсов путём комбинирования простых компонентов. Наш попап-компонент должен следовать этой философии. Для максимальной гибкости и переиспользуемости мы создадим попап как ViewModifier. Это позволит применять его к любым представлениям с помощью синтаксиса модификаторов:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Popup<T: View>: ViewModifier {
    let popup: T
    let isPresented: Bool
    
    init(isPresented: Bool, @ViewBuilder content: () -> T) {
        self.isPresented = isPresented
        popup = content()
    }
    
    func body(content: Content) -> some View {
        content
            .overlay(popupContent())
    }
    
    @ViewBuilder private func popupContent() -> some View {
        // Здесь будет реализация с GeometryReader
    }
}
Обратите внимание на атрибут @ViewBuilder. Он позволяет использовать декларативный синтаксис SwiftUI при определении содержимого попапа. Благодаря этому мы можем создавать сложные попапы с множеством вложенных элементов.

Управление жизненным циклом попап-компонентов требует особого внимания, особенно в многоэкранных приложениях. В отличие от UIKit, где каждый view controller имеет чётко определённый жизненный цикл, в SwiftUI представления могут создаваться и уничтожаться системой оптимизации в любой момент. Для нашего попапа это означает, что мы должны быть осторожны с состоянием. Например, если попап содержит форму ввода или анимации, мы не должны полагаться на сохранение его внутреннего состояния между перерисовками. Вместо этого стоит использовать @State, @StateObject или другие механизмы управления состоянием, предоставляемые SwiftUI.

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ContentView: View {
    @State private var showPopup = false
    @State private var popupText = ""
    
    var body: some View {
        VStack {
            Button("Показать попап") {
                popupText = "Текущее время: \(Date())"
                showPopup = true
            }
        }
        .modifier(Popup(isPresented: showPopup) {
            Text(popupText)
                .padding()
                .background(Color.white)
                .cornerRadius(10)
                .shadow(radius: 5)
        })
    }
}
В этом примере мы храним как состояние видимости попапа (showPopup), так и данные, которые он отображает (popupText), в родительском представлении. Это гарантирует, что информация сохранится даже если сам попап будет уничтожен и создан заново. Ещё один аспект жизненного цикла – обработка появления и исчезновения попапа. SwiftUI предоставляет модификаторы .onAppear() и .onDisappear(), которые можно использовать для выполнения действий при появлении или исчезновении представления. Но есть нюанс: эти модификаторы сработают только когда представление добавляется в иерархию или удаляется из неё.

В случае с нашими попапами мы можем столкнуться с ситуацией, когда isPresented меняется с false на true, но попап уже не находится в иерархии представлений (условие if showPopup в нашем примере). В таком случае модификаторы .onAppear() не сработают сразу. Чтобы решить эту проблему, можно использовать модификатор .onChange() для отслеживания изменения состояния:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct ContentView: View {
    @State private var showPopup = false
    
    var body: some View {
        Button("Показать попап") {
            showPopup = true
        }
        .modifier(Popup(isPresented: showPopup) {
            Text("Привет, мир!")
                .padding()
                .background(Color.white)
                .cornerRadius(10)
                .onChange(of: showPopup) { newValue in
                    if newValue {
                        // Попап появился
                    } else {
                        // Попап исчез
                    }
                }
        })
    }
}
При разработке переиспользуемых компонентов важно учитывать доступ к EnvironmentValues. В SwiftUI, EnvironmentValues позволяют передавать данные вниз по иерархии представлений без необходимости явно передавать их через параметры.

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

Swift Скопировано
1
2
3
4
5
6
7
.modifier(Popup(isPresented: showPopup) {
    Text("Привет, мир!")
        .padding()
        .background(Color.white)
        .cornerRadius(10)
        .environment(\.colorScheme, .light)
})
Модификатор .environment() позволяет переопределить значения окружения для конкретного представления и всех его дочерних элементов. Ещё один важный аспект архитектуры попапов в SwiftUI – это взаимодействие с окружающим контентом. Когда попап отображается, мы часто хотим, чтобы основной контент был затемнён или заблокирован. Для этого можно добавить полупрозрачный фон с обработчиком нажатия:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            ZStack {
                // Затемнённый фон
                Color.black.opacity(0.4)
                    .onTapGesture {
                        // Закрыть попап при нажатии на фон
                        // Здесь нужно будет добавить коллбэк
                    }
                
                // Содержимое попапа
                popup
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
            }
        }
    }
}
Обратите внимание, что в этом примере нам нужен способ закрыть попап при нажатии на затемнённый фон. Поскольку isPresented является входным параметром для нашего модификатора, мы не можем изменить его напрямую. Вместо этого мы можем добавить коллбэк в наш модификатор:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Popup<T: View>: ViewModifier {
    let popup: T
    let isPresented: Bool
    let alignment: Alignment
    let onDismiss: (() -> Void)?
    
    init(isPresented: Bool, alignment: Alignment = .center, onDismiss: (() -> Void)? = nil, @ViewBuilder content: () -> T) {
        self.isPresented = isPresented
        self.alignment = alignment
        self.onDismiss = onDismiss
        popup = content()
    }
    
    // ...
}
И теперь мы можем использовать этот коллбэк в обработчике нажатия:

Swift Скопировано
1
2
3
4
Color.black.opacity(0.4)
    .onTapGesture {
        onDismiss?()
    }
А при использовании модификатора мы передаём функцию, которая изменяет состояние:

Swift Скопировано
1
2
3
.modifier(Popup(isPresented: showPopup, onDismiss: { showPopup = false }) {
    // Содержимое попапа
})
Для удобства использования нашего модификатора мы можем создать расширение для View:

Swift Скопировано
1
2
3
4
5
extension View {
    func popup<T: View>(isPresented: Bool, alignment: Alignment = .center, onDismiss: (() -> Void)? = nil, @ViewBuilder content: () -> T) -> some View {
        self.modifier(Popup(isPresented: isPresented, alignment: alignment, onDismiss: onDismiss, content: content))
    }
}
Это позволит использовать наш попап как обычный модификатор вида:

Swift Скопировано
1
2
3
4
ContentView()
    .popup(isPresented: $showPopup) {
        Text("Привет, мир!")
    }
При проектировании попап-компонентов часто возникает вопрос о том, как обрабатывать вложенность. Что произойдёт, если мы захотим показать один попап поверх другого? В SwiftUI нет встроенного механизма управления стеком модальных представлений, как в UIKit. Вместо этого нам нужно разработать свою стратегию. Один из подходов – использовать уникальные идентификаторы для каждого попапа и отслеживать их в массиве:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
struct PopupManager: EnvironmentKey {
    static let defaultValue: [String: Bool] = [:]
}
 
extension EnvironmentValues {
    var popupManager: [String: Bool] {
        get { self[PopupManager.self] }
        set { self[PopupManager.self] = newValue }
    }
}
Затем мы можем использовать этот менеджер для отслеживания состояния различных попапов:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct ContentView: View {
    @Environment(\.popupManager) var popupManager
    @State private var showFirstPopup = false
    @State private var showSecondPopup = false
    
    var body: some View {
        VStack {
            Button("Показать первый попап") {
                showFirstPopup = true
            }
        }
        .popup(isPresented: showFirstPopup, onDismiss: { showFirstPopup = false }) {
            VStack {
                Text("Первый попап")
                Button("Показать второй попап") {
                    showSecondPopup = true
                }
            }
            .popup(isPresented: showSecondPopup, onDismiss: { showSecondPopup = false }) {
                Text("Второй попап")
            }
        }
    }
}
Это простой пример, но в реальном приложении мы можем расширить этот подход, добавив возможность управления очередью попапов, их приоритетами и анимациями перехода между ними. Ещё одна важная характеристика хорошо спроектированного попап-компонента – способность адаптироваться к различным размерам экрана и ориентациям устройства. В SwiftUI мы можем использовать комбинацию GeometryReader и модификаторов размера и положения для достижения этой цели:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            ZStack {
                Color.black.opacity(0.4)
                    .onTapGesture { onDismiss?() }
                
                popup
                    .padding()
                    .background(
                        RoundedRectangle(cornerRadius: 10)
                            .fill(Color.white)
                            .shadow(radius: 10)
                    )
                    .frame(
                        width: min(geometry.size.width * 0.8, 400),
                        height: min(geometry.size.height * 0.8, 600),
                        alignment: alignment
                    )
                    .position(
                        x: geometry.size.width / 2,
                        y: geometry.size.height / 2
                    )
            }
        }
    }
}
В этом примере мы ограничиваем размер попапа, чтобы он не занимал весь экран на больших устройствах, но при этом масштабировался пропорционально на маленьких экранах.

Интегрировать UIKit в SwiftUI или наоборот?
Я начинающий разработчик под Apple. Насколько я понимаю, UIKit и SwiftUI – это полностью самостоятельные фреймворки, на каждом из которых можно...

Работа с изображениями SwiftUI
Всем добрый день! Вот в продолжении обучения по теме swiftUI столкнулся со следующей проблемой: extension Landmark { var image: Image { ...

SwiftUI текущий календарь
Добрый день. Изучаю SwiftUI и столкнулся с ошибкой в строке: let calendar = Calendar.current Описание ошибки: Type 'Calendar' has no member...

SwiftUI <Клавиатура>
Версия XCode 12.5.1 Упёрся в то что как-то не получается показать клавиатуру в самом простом коде Хоть сколько &lt;тапай&gt; по...


Реализация базового попапа



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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Popup<T: View>: ViewModifier {
    let popup: T
    let isPresented: Bool
 
    // 1. Инициализатор с использованием ViewBuilder для содержимого
    init(isPresented: Bool, @ViewBuilder content: () -> T) {
        self.isPresented = isPresented
        popup = content()
    }
 
    // 2. Основная функция модификатора
    func body(content: Content) -> some View {
        content
            .overlay(popupContent())
    }
 
    // 3. Метод для создания содержимого попапа
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                popup
                    .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
    }
}
Здесь происходит следующее:
1. Мы инициализируем попап с двумя параметрами: флагом isPresented, указывающим, видим ли попап на экране, и замыканием content, которое создаёт содержимое попапа. Атрибут @ViewBuilder позволяет использовать декларативный синтаксис SwiftUI при определении содержимого.
2. В методе body(content:) мы добавляем попап как наложение к родительскому представлению с помощью модификатора .overlay().
3. Метод popupContent() использует GeometryReader для определения размера доступного пространства и отображает попап только если isPresented равно true.
Давайте проверим наш компонент в превью:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
struct PopupPreview: PreviewProvider {
    static var previews: some View {
        Color.clear
            .modifier(Popup(isPresented: true) {
                Color.yellow
                    .frame(width: 100, height: 100)
            })
            .previewDevice("iPhone 13")
    }
}
Мы увидим жёлтый квадрат размером 100×100 пикселей, центрированный на экране. Но это только начало – сейчас наш попап ещё очень примитивен.

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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Popup<T: View>: ViewModifier {
    let popup: T
    let isPresented: Bool
    let alignment: Alignment  // Новый параметр
 
    init(isPresented: Bool, alignment: Alignment = .center, @ViewBuilder content: () -> T) {
        self.isPresented = isPresented
        self.alignment = alignment
        popup = content()
    }
 
    // ...
 
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                popup
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
            }
        }
    }
}
Теперь мы можем позиционировать наш попап в любом месте экрана, используя стандартные значения Alignment из SwiftUI:

Swift Скопировано
1
2
3
4
5
Color.clear
    .modifier(Popup(isPresented: true, alignment: .topTrailing) {
        Color.orange
            .frame(width: 100, height: 100)
    })
Но статичный попап – это только половина дела. Настоящая магия начинается, когда мы добавляем анимацию. Пользователи ожидают, что интерфейс будет плавным и отзывчивым, и хорошо анимированный попап может значительно улучшить впечатление от использования приложения. Добавим переход с анимацией к нашему попапу:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            popup
                .animation(.spring())  // 1. Добавляем анимацию
                .transition(.offset(x: 0, y: geometry.belowScreenEdge))  // 2. Задаём переход
                .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
        }
    }
}
 
// 3. Расширение для определения положения ниже края экрана
private extension GeometryProxy {
    var belowScreenEdge: CGFloat {
        UIScreen.main.bounds.height - frame(in: .global).minY
    }
}
Что здесь происходит:

1. Модификатор .animation(.spring()) указывает SwiftUI анимировать любые изменения анимируемых свойств представления. В нашем случае мы будем анимировать позицию.
2. Модификатор .transition(.offset(x: 0, y: geometry.belowScreenEdge)) определяет, как представление появляется и исчезает. Сам по себе он не имеет эффекта и должен использоваться вместе с анимацией. В нашем случае мы задаём смещение, которое перемещает попап от нижнего края экрана.
3. Расширение GeometryProxy позволяет нам рассчитать позицию ниже экрана для начальной точки анимации.

Чтобы увидеть анимацию в действии, добавим кнопку для переключения видимости попапа:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct AnimatedPopupPreview: View {
    @State var isPresented = false
 
    var body: some View {
        ZStack {
            Color.clear
            VStack {
                Button("Показать/скрыть") {
                    isPresented.toggle()
                }
                Spacer()
            }
        }
        .modifier(Popup(isPresented: isPresented, alignment: .center) {
            Color.yellow
                .frame(width: 100, height: 100)
        })
    }
}
Но что, если мы хотим, чтобы попап появлялся не снизу, а сверху экрана? Добавим возможность выбора направления анимации:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension Popup {
    enum Direction {
        case top, bottom
 
        func offset(popupFrame: CGRect) -> CGFloat {
            switch self {
            case .top:
                let aboveScreenEdge = -popupFrame.maxY
                return aboveScreenEdge
            case .bottom:
                let belowScreenEdge = UIScreen.main.bounds.height - popupFrame.minY
                return belowScreenEdge
            }
        }
    }
}
И модифицируем наш ViewModifier:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Popup<T: View>: ViewModifier {
    let popup: T
    let isPresented: Bool
    let alignment: Alignment
    let direction: Direction
 
    init(isPresented: Bool, alignment: Alignment = .center, direction: Direction = .bottom, @ViewBuilder content: () -> T) {
        self.isPresented = isPresented
        self.alignment = alignment
        self.direction = direction
        popup = content()
    }
 
    // ...
 
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                popup
                    .animation(.spring())
                    .transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
            }
        }
    }
}
Теперь мы можем создавать попапы, которые появляются как снизу, так и сверху экрана:

Swift Скопировано
1
2
3
4
5
6
7
.modifier(Popup(isPresented: isPresented, alignment: .top, direction: .top) {
    Text("Верхний попап")
        .padding()
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 5)
})
Наш базовый попап уже достаточно функционален, но давайте добавим ещё одну важную деталь – возможность закрыть попап при нажатии вне его области. Для этого нам понадобится затемнённый фон, который будет реагировать на касания:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Popup<T: View>: ViewModifier {
    // ...
    let onDismiss: (() -> Void)?
 
    init(isPresented: Bool, alignment: Alignment = .center, direction: Direction = .bottom, onDismiss: (() -> Void)? = nil, @ViewBuilder content: () -> T) {
        self.isPresented = isPresented
        self.alignment = alignment
        self.direction = direction
        self.onDismiss = onDismiss
        popup = content()
    }
 
    // ...
 
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                ZStack {
                    // Затемнённый фон
                    Color.black.opacity(0.4)
                        .onTapGesture {
                            onDismiss?()
                        }
                    
                    // Содержимое попапа
                    popup
                        .animation(.spring())
                        .transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
                }
                .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
            }
        }
    }
}
Для удобства использования создадим расширение для View, которое будет применять наш модификатор:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
extension View {
    func popup<T: View>(
        isPresented: Bool,
        alignment: Alignment = .center,
        direction: Popup<T>.Direction = .bottom,
        onDismiss: (() -> Void)? = nil,
        @ViewBuilder content: () -> T
    ) -> some View {
        modifier(Popup(isPresented: isPresented, alignment: alignment, direction: direction, onDismiss: onDismiss, content: content))
    }
}
Теперь мы можем использовать наш попап как обычный модификатор:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ContentView: View {
    @State private var showPopup = false
 
    var body: some View {
        Button("Показать попап") {
            showPopup = true
        }
        .popup(isPresented: showPopup, onDismiss: { showPopup = false }) {
            VStack {
                Text("Это попап!")
                    .font(.headline)
                Button("Закрыть") {
                    showPopup = false
                }
                .padding()
            }
            .padding()
            .background(Color.white)
            .cornerRadius(10)
            .shadow(radius: 5)
        }
    }
}
Вот мы и создали базовый, но полностью функциональный попап-компонент, который можно использовать в различных сценариях. Он поддерживает:
  • Разное позиционирование.
  • Анимацию появления и исчезновения.
  • Выбор направления анимации.
  • Закрытие при нажатии вне области попапа.

Что ещё более важно, мы спроектировали его как переиспользуемый компонент, который легко интегрировать в существующие представления с помощью удобного модификатора .popup().

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

Обработка жестов в попапе



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

Одна из распространённых механик для закрытия попапа – жест смахивания вниз. Добавим эту возможность в наш компонент:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            ZStack {
                // Затемнённый фон
                Color.black.opacity(0.4)
                    .onTapGesture {
                        onDismiss?()
                    }
                
                // Содержимое попапа
                popup
                    .animation(.spring())
                    .transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
                    .gesture(
                        DragGesture()
                            .onEnded { value in
                                if value.translation.height > 50 {
                                    onDismiss?()
                                }
                            }
                    )
            }
            .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
        }
    }
}
Теперь пользователь может закрыть попап, смахнув его вниз. Мы проверяем, что смещение по вертикали больше 50 пунктов, чтобы отличить намеренный жест закрытия от случайного касания. Однако это решение не очень элегантно – жест становится активным только после завершения. Было бы лучше, если бы пользователь видел, как попап следует за его пальцем во время жеста. Для этого нам нужно управлять положением попапа напрямую:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@State private var dragOffset: CGSize = .zero
 
// ...
 
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            ZStack {
                // Затемнённый фон
                Color.black.opacity(0.4)
                    .onTapGesture {
                        onDismiss?()
                    }
                
                // Содержимое попапа
                popup
                    .offset(y: dragOffset.height > 0 ? dragOffset.height : 0)
                    .animation(.spring())
                    .transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                dragOffset = value.translation
                            }
                            .onEnded { value in
                                if value.translation.height > 50 {
                                    onDismiss?()
                                }
                                dragOffset = .zero
                            }
                    )
            }
            .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
        }
    }
}
Теперь мы добавили состояние dragOffset, которое отслеживает смещение при перетаскивании, и применяем его к положению попапа только если смещение положительное (вниз). Это создаёт естественное ощущение при взаимодействии – попап как бы "прилипает" к краю экрана при попытке потянуть его вверх, но свободно двигается вниз.

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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
Button("Закрыть") {
    showPopup = false
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
.scaleEffect(buttonPressed ? 0.95 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6))
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: 50, pressing: { pressing in
    buttonPressed = pressing
}, perform: {})
Эта техника добавляет тактильный отклик при нажатии – кнопка немного уменьшается, создавая иллюзию физического взаимодействия.

Ещё одно важное улучшение – это предотвращение случайного закрытия попапа при взаимодействии с его содержимым. По умолчанию жест касания для закрытия попапа будет срабатывать даже при нажатии на сам попап из-за наложения элементов в ZStack. Чтобы исправить это, мы можем добавить прозрачную область, которая блокирует передачу касаний:

Swift Скопировано
1
2
3
4
5
6
// Содержимое попапа
popup
    .background(Color.white.opacity(0.001))
    .onTapGesture {} // Предотвращает передачу касания на фон
    .offset(y: dragOffset.height > 0 ? dragOffset.height : 0)
    // остальные модификаторы
Безопасный доступ к EnvironmentValues – ещё один важный аспект при реализации попапов. Как мы уже упоминали, попап наследует значения окружения от родительского представления. Однако иногда нам нужно переопределить некоторые из них. Например, представим, что наш попап содержит форму для ввода текста с клавиатуры. В этом случае нам может понадобиться управление способом отображения клавиатуры:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct KeyboardAwarePopup<T: View>: ViewModifier {
    // ... остальные свойства как в Popup
    
    @State private var keyboardHeight: CGFloat = 0
    
    func body(content: Content) -> some View {
        content
            .overlay(popupContent())
            .onReceive(
                NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
                    .map { notification -> CGFloat in
                        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
                    }
            ) { height in
                self.keyboardHeight = height
            }
            .onReceive(
                NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            ) { _ in
                self.keyboardHeight = 0
            }
    }
    
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                ZStack {
                    // ... остальной код как раньше
                    
                    // Содержимое попапа
                    popup
                        .padding(.bottom, keyboardHeight)
                        // остальные модификаторы
                }
                // ... остальной код
            }
        }
    }
}
Этот модификатор отслеживает появление и исчезновение клавиатуры, и автоматически регулирует позицию попапа, чтобы он не перекрывался клавиатурой. В некоторых случаях нам может потребоваться полный контроль над фоном попапа. Например, мы хотим использовать размытие (blur) вместо простого затемнения:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            ZStack {
                // Размытый фон
                VisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
                    .opacity(0.8)
                    .onTapGesture {
                        onDismiss?()
                    }
                
                // Содержимое попапа
                // ... остальной код
            }
            // ... остальной код
        }
    }
}
 
// Обёртка для UIVisualEffectView
struct VisualEffectView: UIViewRepresentable {
    let effect: UIVisualEffect
    
    func makeUIView(context: Context) -> UIVisualEffectView {
        return UIVisualEffectView(effect: effect)
    }
    
    func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
        uiView.effect = effect
    }
}
Это создаёт более современный, стеклянный вид фона, который стал популярным в последних версиях iOS.

Особый случай, который требует внимания – это попапы внутри ScrollView или List. Когда попап находится внутри прокручиваемого представления, возникает ряд проблем:
1. Попап может прокручиваться вместе с содержимым.
2. Жесты прокрутки могут конфликтовать с жестами попапа.
3. Позиционирование становится более сложным.

Чтобы решить эти проблемы, мы можем создать специальный модификатор для использования в контексте прокрутки:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct ScrollAwarePopup<T: View>: ViewModifier {
    // ... остальные свойства как в Popup
    
    @State private var contentOffset: CGPoint = .zero
    
    func body(content: Content) -> some View {
        content
            .overlay(
                GeometryReader { geometry in
                    Color.clear
                        .preference(
                            key: ViewPositionKey.self,
                            value: geometry.frame(in: .global).origin
                        )
                }
            )
            .onPreferenceChange(ViewPositionKey.self) { position in
                contentOffset = position
            }
            .overlay(popupContent())
    }
    
    @ViewBuilder private func popupContent() -> some View {
        if isPresented {
            ZStack {
                // ... остальной код
            }
            .position(
                x: UIScreen.main.bounds.width / 2,
                y: UIScreen.main.bounds.height / 2
            )
            .ignoresSafeArea()
        }
    }
}
 
struct ViewPositionKey: PreferenceKey {
    static var defaultValue: CGPoint = .zero
    
    static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
        value = nextValue()
    }
}
Этот модификатор отслеживает положение содержимого и позиционирует попап относительно экрана, а не родительского представления. Таким образом попап остаётся неподвижным при прокрутке.
Наконец, давайте реализуем вариант попапа, который сохраняет своё положение при изменении ориентации устройства:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct OrientationAwarePopup<T: View>: ViewModifier {
    // ... остальные свойства
    
    @State private var orientation = UIDevice.current.orientation
    
    func body(content: Content) -> some View {
        content
            .overlay(popupContent())
            .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                orientation = UIDevice.current.orientation
            }
    }
    
    // ... остальной код
}
Теперь наш попап будет корректно обрабатывать изменения ориентации, пересчитывая своё положение при повороте устройства.

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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension View {
    func advancedPopup<T: View>(
        isPresented: Bool,
        alignment: Alignment = .center,
        direction: Popup<T>.Direction = .bottom,
        blurBackground: Bool = false,
        keyboardAware: Bool = false,
        onDismiss: (() -> Void)? = nil,
        @ViewBuilder content: () -> T
    ) -> some View {
        // Комбинирование нужных модификаторов в зависимости от параметров
        // ...
    }
}
Такой подход дает максимальную гибкость при создании пользовательского интерфейса и позволяет легко адаптировать попапы под различные сценарии использования.

Особенности позиционирования



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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
struct AnchoredPopup<T: View, A: View>: ViewModifier {
    let popup: T
    let anchor: A
    let isPresented: Bool
    let position: Position
    let offset: CGPoint
    
    enum Position {
        case top, bottom, left, right
    }
    
    func body(content: Content) -> some View {
        content
            .overlay(
                ZStack {
                    if isPresented {
                        GeometryReader { geometry in
                            anchoredPopupContent(in: geometry)
                        }
                    }
                }
            )
    }
    
    @ViewBuilder private func anchoredPopupContent(in geometry: GeometryProxy) -> some View {
        // Находим фрейм якоря в глобальных координатах
        let anchorFrame = findAnchorFrame(in: geometry)
        
        if let anchorFrame = anchorFrame {
            popup
                .background(Color.white)
                .cornerRadius(8)
                .shadow(radius: 4)
                .position(calculatePosition(for: anchorFrame))
        }
    }
    
    private func findAnchorFrame(in geometry: GeometryProxy) -> CGRect? {
        // Этот метод будет использовать PreferenceKey для определения позиции якоря
        // Упрощённая логика для примера
        return nil 
    }
    
    private func calculatePosition(for anchorFrame: CGRect) -> CGPoint {
        var x = anchorFrame.midX
        var y = anchorFrame.midY
        
        switch position {
        case .top:
            y = anchorFrame.minY - offset.y
        case .bottom:
            y = anchorFrame.maxY + offset.y
        case .left:
            x = anchorFrame.minX - offset.x
        case .right:
            x = anchorFrame.maxX + offset.x
        }
        
        return CGPoint(x: x, y: y)
    }
}
Эта реализация даёт нам возможность привязывать попап к конкретному элементу интерфейса. Но для её полноценной работы нам нужно научиться отслеживать позицию этого элемента. В SwiftUI это делается с помощью системы предпочтений (Preference System):

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct AnchorPreferenceKey: PreferenceKey {
    static var defaultValue: [String: Anchor<CGRect>] = [:]
    
    static func reduce(value: inout [String: Anchor<CGRect>], nextValue: () -> [String: Anchor<CGRect>]) {
        value.merge(nextValue()) { $1 }
    }
}
 
extension View {
    func anchorPreference(id: String) -> some View {
        self.anchorPreference(key: AnchorPreferenceKey.self, value: .bounds) { 
            [id: $0]
        }
    }
}
Теперь мы можем пометить любой элемент как якорь для нашего попапа и отслеживать его позицию:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct ContentView: View {
    @State private var showPopup = false
    
    var body: some View {
        VStack {
            Text("Нажмите на кнопку")
            
            Button("Показать попап") {
                showPopup = true
            }
            .anchorPreference(id: "popupAnchor")
        }
        .overlayPreferenceValue(AnchorPreferenceKey.self) { preferences in
            GeometryReader { geometry in
                if showPopup, let anchor = preferences["popupAnchor"] {
                    let rect = geometry[anchor]
                    
                    Text("Это привязанный попап!")
                        .padding()
                        .background(Color.white)
                        .cornerRadius(8)
                        .shadow(radius: 4)
                        .position(x: rect.midX, y: rect.minY - 10)
                }
            }
        }
    }
}
Этот подход даёт нам прецизионный контроль над положением попапа, но приходится жертвовать простотой использования. К счастью, мы можем объединить эту технику с нашим предыдущим решением, создав комплексный API.

Работа с безопасными зонами представляет отдельную проблему, особенно на современных устройствах с Dynamic Island, вырезами и закруглёнными углами экрана. По умолчанию наш попап размещается внутри безопасной зоны, но иногда мы хотим, чтобы он занимал всё пространство экрана, включая области за пределами safe area.

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ViewBuilder private func popupContent() -> some View {
    GeometryReader { geometry in
        if isPresented {
            ZStack {
                // Затемнённый фон
                Color.black.opacity(0.4)
                    .edgesIgnoringSafeArea(.all) // Игнорируем safe area для фона
                    .onTapGesture {
                        onDismiss?()
                    }
                
                // Содержимое попапа
                popup
                    .padding(.horizontal) // Гарантируем отступ от краёв экрана
                    .padding(.top, geometry.safeAreaInsets.top) // Учитываем верхнюю safe area
                    .padding(.bottom, geometry.safeAreaInsets.bottom) // Учитываем нижнюю safe area
            }
        }
    }
}
Для обеспечения корректного отображения на устройствах с Dynamic Island нам нужно быть особенно внимательными. Если попап позиционируется сверху экрана, он должен учитывать размер и положение Dynamic Island:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
private func calculateTopSafeOffset(in geometry: GeometryProxy) -> CGFloat {
    // Проверяем, есть ли Dynamic Island
    let hasDynamicIsland = UIDevice.current.userInterfaceIdiom == .phone && 
                           geometry.safeAreaInsets.top > 50
    
    if hasDynamicIsland {
        // На устройствах с Dynamic Island оставляем дополнительный отступ
        return geometry.safeAreaInsets.top + 10
    } else {
        // На остальных устройствах используем стандартный отступ
        return geometry.safeAreaInsets.top
    }
}
При работе со сложными попапами, особенно теми, что содержат длинный текст или списки, важно контролировать их размеры. Адаптивное позиционирование для разных размеров экранов – ещё одна головная боль разработчика:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ViewBuilder private func adaptivePopupContent(in geometry: GeometryProxy) -> some View {
    // Рассчитываем максимальные размеры попапа
    let maxWidth = min(geometry.size.width * 0.9, 400)
    let maxHeight = min(geometry.size.height * 0.8, 600)
    
    // Создаём ScrollView, если контент не помещается
    popup
        .frame(maxWidth: maxWidth, maxHeight: maxHeight)
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(Color.white)
                .shadow(radius: 8)
        )
        .position(
            x: geometry.size.width / 2,
            y: geometry.size.height / 2
        )
}
Особую проблему представляет ситуация, когда попап должен изменять своё положение в зависимости от доступного пространства. Например, если попап привязан к элементу в нижней части экрана, он должен отображаться над этим элементом, а не под ним, чтобы избежать обрезания содержимого:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private func calculateAdaptivePosition(anchor: CGRect, in geometry: GeometryProxy, popupSize: CGSize) -> CGPoint {
    let idealX = anchor.midX
    let idealY = anchor.maxY + 10 + popupSize.height / 2 // По умолчанию под якорем
    
    // Проверяем, не выходит ли попап за нижнюю границу экрана
    if idealY + popupSize.height / 2 > geometry.size.height - geometry.safeAreaInsets.bottom {
        // Если выходит, размещаем его над якорем
        return CGPoint(
            x: idealX,
            y: anchor.minY - 10 - popupSize.height / 2
        )
    }
    
    // По умолчанию возвращаем исходную позицию
    return CGPoint(x: idealX, y: idealY)
}
Проблемы наложения и z-индекса в SwiftUI решаются не так интуитивно, как в CSS. В SwiftUI порядок наложения определяется порядком объявления представлений в коде – представления, объявленные позже, отображаются поверх представлений, объявленных ранее. Но что если нам нужно показать несколько попапов одновременно с определённым порядком наложения?

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct PopupContainer: ViewModifier {
    @Binding var activePopups: [PopupItem]
    
    func body(content: Content) -> some View {
        content
            .overlayWithPopups()
    }
    
    private func overlayWithPopups() -> some ViewModifier {
        return _OverlayModifier { content in
            ZStack {
                content
                
                ForEach(activePopups.sorted(by: { $0.zIndex < $1.zIndex })) { item in
                    item.view
                        .zIndex(Double(item.zIndex))
                }
            }
        }
    }
    
    private struct _OverlayModifier: ViewModifier {
        let overlay: (Content) -> AnyView
        
        init<Overlay: View>(overlay: @escaping (Content) -> Overlay) {
            self.overlay = { AnyView(overlay($0)) }
        }
        
        func body(content: Content) -> some View {
            overlay(content)
        }
    }
}
 
struct PopupItem: Identifiable {
    let id = UUID()
    let view: AnyView
    let zIndex: Int
    
    init<V: View>(view: V, zIndex: Int = 0) {
        self.view = AnyView(view)
        self.zIndex = zIndex
    }
}
Этот подход позволяет управлять стеком попапов на глобальном уровне, программно контролируя их z-индекс и последовательность отображения.

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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ContentSizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}
 
extension View {
    func reportContentSize() -> some View {
        self.background(
            GeometryReader { geometry in
                Color.clear
                    .preference(
                        key: ContentSizePreferenceKey.self,
                        value: geometry.size
                    )
            }
        )
    }
}
Теперь мы можем отслеживать изменения размера содержимого попапа и корректировать его положение:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct DynamicPositionPopup<T: View>: ViewModifier {
    // Обычные свойства попапа...
    @State private var contentSize: CGSize = .zero
    
    func body(content: Content) -> some View {
        content
            .overlay(popupContent())
    }
    
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                ZStack {
                    // Затемнённый фон...
                    
                    popup
                        .reportContentSize()
                        .onPreferenceChange(ContentSizePreferenceKey.self) { size in
                            contentSize = size
                        }
                        .position(
                            calculateOptimalPosition(
                                containerSize: geometry.size,
                                contentSize: contentSize,
                                safeAreaInsets: geometry.safeAreaInsets
                            )
                        )
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
    }
    
    private func calculateOptimalPosition(containerSize: CGSize, contentSize: CGSize, safeAreaInsets: EdgeInsets) -> CGPoint {
        // Рассчитываем оптимальную позицию в зависимости от размера контента
        // и доступного пространства
        var x = containerSize.width / 2
        var y = containerSize.height / 2
        
        // Если попап слишком высокий, смещаем его вверх
        if contentSize.height > containerSize.height * 0.7 {
            y = containerSize.height * 0.4
        }
        
        // Если попап слишком широкий, центрируем его горизонтально
        if contentSize.width > containerSize.width * 0.9 {
            x = containerSize.width / 2
        }
        
        return CGPoint(x: x, y: y)
    }
}
При использовании этого подхода попап будет автоматически корректировать своё положение при изменении содержимого, обеспечивая оптимальное размещение на экране.

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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct OverflowPopup: UIViewRepresentable {
    let content: UIView
    let frame: CGRect
    
    func makeUIView(context: Context) -> UIView {
        let containerView = UIView(frame: .zero)
        containerView.backgroundColor = .clear
        containerView.isUserInteractionEnabled = true
        
        let popupView = content
        popupView.frame = frame
        popupView.layer.cornerRadius = 8
        popupView.layer.shadowColor = UIColor.black.cgColor
        popupView.layer.shadowOpacity = 0.2
        popupView.layer.shadowOffset = CGSize(width: 0, height: 2)
        popupView.layer.shadowRadius = 4
        
        containerView.addSubview(popupView)
        
        return containerView
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        if let popupView = uiView.subviews.first {
            popupView.frame = frame
        }
    }
}
 
// Использование:
struct ContentView: View {
    @State private var showPopup = false
    @State private var buttonFrame: CGRect = .zero
    
    var body: some View {
        Button("Показать подсказку") {
            showPopup.toggle()
        }
        .background(
            GeometryReader { geometry in
                Color.clear
                    .onAppear {
                        buttonFrame = geometry.frame(in: .global)
                    }
            }
        )
        .overlay(
            showPopup ? AnyView(
                OverflowPopup(
                    content: UIHostingController(
                        rootView: Text("Это подсказка, которая может выходить за границы представления")
                            .padding()
                            .background(Color.white)
                    ).view,
                    frame: CGRect(
                        x: buttonFrame.midX - 100,
                        y: buttonFrame.maxY + 10,
                        width: 200,
                        height: 100
                    )
                )
            ) : AnyView(EmptyView())
        )
    }
}
Этот подход даёт нам полный контроль над позиционированием, но требует больше кода и смешивания SwiftUI с UIKit. Другой подход — использование расширенных возможностей системы координат SwiftUI. Когда мы работаем с GeometryReader, мы имеем доступ к нескольким системам координат: .global, .local и .named. Умелое использование этих систем координат позволяет решить многие проблемы позиционирования:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct CoordinateSpacePopup<T: View>: ViewModifier {
    // Обычные свойства попапа...
    let coordinateSpace: CoordinateSpace
    
    @ViewBuilder private func popupContent() -> some View {
        GeometryReader { geometry in
            if isPresented {
                popup
                    .position(
                        x: geometry.frame(in: coordinateSpace).midX,
                        y: geometry.frame(in: coordinateSpace).midY
                    )
            }
        }
    }
}
При создании сложных интерфейсов с множеством вложенных попапов стоит обратить внимание на подход с именованными пространствами координат:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
ZStack {
    ScrollView {
        // Основное содержимое...
    }
    
    if showPopup {
        popupContent
            .position(x: position.x, y: position.y)
    }
}
.coordinateSpace(name: "mainSpace")
Это позволяет позиционировать попап относительно фиксированной системы координат, даже если структура интерфейса сложная и многоуровневая.
При разработке для разных устройств особенно важно учитывать не только различия в размерах экранов, но и особенности каждой платформы. Попап, который прекрасно смотрится на iPhone, может выглядеть странно на iPad или Mac:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private func adaptivePopupStyle() -> some ViewModifier {
    Group {
        #if os(iOS)
        if UIDevice.current.userInterfaceIdiom == .pad {
            // Стиль для iPad
            PadPopupStyle()
        } else {
            // Стиль для iPhone
            PhonePopupStyle()
        }
        #elseif os(macOS)
        // Стиль для Mac
        MacPopupStyle()
        #else
        // Дефолтный стиль
        DefaultPopupStyle()
        #endif
    }
}
Мы можем оформить эту логику в виде модификатора, который будет применяться к содержимому попапа:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
extension View {
    func adaptivePopupStyle() -> some View {
        modifier(AdaptivePopupStyleModifier())
    }
}
 
struct AdaptivePopupStyleModifier: ViewModifier {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    func body(content: Content) -> some View {
        Group {
            if horizontalSizeClass == .regular {
                // Стиль для больших экранов
                content
                    .frame(minWidth: 320, maxWidth: 420)
                    .cornerRadius(16)
                    .shadow(radius: 12)
            } else {
                // Стиль для компактных экранов
                content
                    .frame(maxWidth: .infinity)
                    .cornerRadius(12)
                    .shadow(radius: 8)
            }
        }
    }
}
Позиционирование попапов с учётом ориентации устройства требует особого внимания. При повороте устройства не только меняются размеры экрана, но и происходит перераспределение безопасных зон

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ViewBuilder private func rotationAwarePopup() -> some View {
    GeometryReader { geometry in
        let isLandscape = geometry.size.width > geometry.size.height
        
        ZStack {
            // Затемнённый фон...
            
            popup
                .padding(isLandscape ? .horizontal : .vertical, 20)
                .padding(isLandscape ? .vertical : .horizontal, 10)
                .frame(
                    width: isLandscape ? nil : min(geometry.size.width * 0.9, 400),
                    height: isLandscape ? min(geometry.size.height * 0.8, 300) : nil
                )
                .position(
                    x: geometry.size.width / 2,
                    y: geometry.size.height / 2
                )
        }
    }
}
Одна из самых сложных задач при позиционировании — корректная работа с устройствами, имеющими нестандартные вырезы экрана, такие как Dynamic Island. При размещении попапа близко к верхнему краю экрана важно не перекрывать эти области:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private func calculateTopPosition(in geometry: GeometryProxy) -> CGPoint {
    let dynamicIslandHeight: CGFloat = 54 // Примерная высота Dynamic Island
    let dynamicIslandWidth: CGFloat = 125 // Примерная ширина Dynamic Island
    
    let hasDynamicIsland = geometry.safeAreaInsets.top > 50
    
    let y = geometry.safeAreaInsets.top + (hasDynamicIsland ? dynamicIslandHeight + 10 : 10)
    
    // Проверяем, не пересекается ли попап с Dynamic Island
    if hasDynamicIsland && abs(geometry.size.width / 2 - dynamicIslandWidth / 2) < contentSize.width / 2 {
        // Если пересекается, смещаем попап ниже
        return CGPoint(x: geometry.size.width / 2, y: y + dynamicIslandHeight / 2)
    }
    
    return CGPoint(x: geometry.size.width / 2, y: y + contentSize.height / 2)
}
Универсальное решение для размещения попапов в сложных сценариях — это создание менеджера попапов, который мог бы централизованно управлять всеми аспектами их отображения:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class PopupManager: ObservableObject {
    @Published var activePopups: [PopupIdentifier: PopupState] = [:]
    
    func show<T: View>(_ popup: T, id: PopupIdentifier, position: PopupPosition) {
        activePopups[id] = PopupState(view: AnyView(popup), position: position)
    }
    
    func update<T: View>(id: PopupIdentifier, view: T) {
        if var state = activePopups[id] {
            state.view = AnyView(view)
            activePopups[id] = state
        }
    }
    
    func hide(id: PopupIdentifier) {
        activePopups.removeValue(forKey: id)
    }
    
    func hideAll() {
        activePopups.removeAll()
    }
}
 
struct PopupState {
    var view: AnyView
    var position: PopupPosition
}
 
enum PopupPosition {
    case center
    case top
    case bottom
    case custom(x: CGFloat, y: CGFloat)
    case anchor(to: CGRect, direction: PopupDirection)
}
 
enum PopupDirection {
    case top, bottom, left, right
}
Использование такого менеджера позволяет создать гибкую систему управления попапами на уровне всего приложения:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct PopupContainerView<Content: View>: View {
    @EnvironmentObject var popupManager: PopupManager
    let content: Content
    
    var body: some View {
        ZStack {
            content
            
            ForEach(Array(popupManager.activePopups.keys), id: \.self) { id in
                if let state = popupManager.activePopups[id] {
                    PopupView(content: state.view, position: state.position)
                        .transition(.opacity.combined(with: .scale))
                        .zIndex(10)
                }
            }
        }
    }
}
Такой подход особенно полезен в приложениях с множеством экранов и сложной логикой отображения попапов.

Практические примеры



Рассмотрим несколько практических примеров которые демонстрируют универсальность созданного решения.

Уведомления и сообщения об ошибках



Одно из наиболее частых применений попапов — отображение уведомлений. Создадим компонент для показа кратковременных сообщений в стиле снэкбаров:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct Snackbar: View {
    let message: String
    let type: SnackbarType
    
    enum SnackbarType {
        case info, success, error, warning
        
        var color: Color {
            switch self {
            case .info: return .blue
            case .success: return .green
            case .error: return .red
            case .warning: return .orange
            }
        }
    }
    
    var body: some View {
        HStack {
            Text(message)
                .foregroundColor(.white)
                .padding()
            Spacer()
        }
        .background(type.color.opacity(0.9))
        .cornerRadius(8)
        .padding(.horizontal)
    }
}
 
// Использование:
struct ContentView: View {
    @State private var showError = false
    
    var body: some View {
        Button("Показать ошибку") {
            showError = true
            
            // Автоматическое скрытие через 3 секунды
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                showError = false
            }
        }
        .popup(isPresented: showError, alignment: .top, direction: .top) {
            Snackbar(message: "Произошла ошибка при загрузке данных", type: .error)
        }
    }
}
Этот снэкбар появляется сверху экрана и автоматически исчезает через три секунды. Благодаря нашей реализации попапа, анимация появления и исчезновения происходит плавно, без дополнительного кода.

Интеграция с системой навигации



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

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct NavigationPopupDemo: View {
    @State private var showDetails = false
    @State private var selectedItem: String? = nil
    
    var body: some View {
        NavigationView {
            List(["Яблоко", "Банан", "Апельсин"], id: \.self) { item in
                Button(item) {
                    selectedItem = item
                    showDetails = true
                }
            }
            .navigationTitle("Фрукты")
            .popup(isPresented: showDetails, onDismiss: { showDetails = false }) {
                VStack {
                    Text("Детали: \(selectedItem ?? "")")
                        .font(.headline)
                    
                    Button("Закрыть") {
                        showDetails = false
                    }
                    .padding()
                }
                .padding()
                .background(Color.white)
                .cornerRadius(10)
                .shadow(radius: 5)
            }
        }
    }
}
В этом примере попап сохраняет своё состояние при навигации благодаря тому, что мы храним selectedItem отдельно от видимости попапа.

Кастомные меню и диалоги



С помощью нашего попап-компонента легко создавать кастомные меню и диалоги подтверждения:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
struct ActionDialog: View {
    let title: String
    let message: String
    let primaryAction: () -> Void
    let secondaryAction: () -> Void
    let cancelAction: () -> Void
    
    var body: some View {
        VStack(spacing: 16) {
            Text(title)
                .font(.headline)
            
            Text(message)
                .font(.body)
                .multilineTextAlignment(.center)
            
            HStack {
                Button("Отмена") {
                    cancelAction()
                }
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.gray.opacity(0.2))
                .cornerRadius(8)
                
                Button("Вторичное") {
                    secondaryAction()
                }
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.blue.opacity(0.2))
                .cornerRadius(8)
                
                Button("Основное") {
                    primaryAction()
                }
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(8)
            }
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(radius: 10)
        .padding(.horizontal, 20)
    }
}

Попапы с формами ввода



Особая категория попапов — формы ввода. Они требуют внимания к работе с клавиатурой и валидации:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
struct FormPopup: View {
    @State private var name = ""
    @State private var email = ""
    @State private var isValid = false
    
    let onSubmit: (String, String) -> Void
    let onCancel: () -> Void
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Регистрация")
                .font(.headline)
            
            TextField("Имя", text: $name)
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
                .onChange(of: name) { _ in validateForm() }
            
            TextField("Email", text: $email)
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
                .keyboardType(.emailAddress)
                .autocapitalization(.none)
                .onChange(of: email) { _ in validateForm() }
            
            HStack {
                Button("Отмена") {
                    onCancel()
                }
                .padding()
                
                Spacer()
                
                Button("Отправить") {
                    onSubmit(name, email)
                }
                .padding()
                .background(isValid ? Color.blue : Color.gray)
                .foregroundColor(.white)
                .cornerRadius(8)
                .disabled(!isValid)
            }
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(radius: 10)
        .padding(.horizontal, 20)
        .keyboardAwarePadding() // Наш кастомный модификатор для работы с клавиатурой
    }
    
    private func validateForm() {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
        let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        
        isValid = !name.isEmpty && 
                 emailPredicate.evaluate(with: email)
    }
}
 
// Модификатор для адаптации к появлению клавиатуры
extension View {
    func keyboardAwarePadding() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAware())
    }
}

Система нотификаций с очередью



Для приложений с интенсивными уведомлениями полезно создать систему с очередью и приоритетами:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class NotificationManager: ObservableObject {
    @Published var notifications: [NotificationItem] = []
    
    func show(message: String, type: Snackbar.SnackbarType, duration: TimeInterval = 3) {
        let item = NotificationItem(id: UUID(), message: message, type: type)
        notifications.append(item)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
            self.notifications.removeAll { $0.id == item.id }
        }
    }
}
 
struct NotificationItem: Identifiable {
    let id: UUID
    let message: String
    let type: Snackbar.SnackbarType
}
 
struct NotificationContainer: ViewModifier {
    @ObservedObject var manager: NotificationManager
    
    func body(content: Content) -> some View {
        content
            .overlay(
                VStack {
                    ForEach(manager.notifications) { item in
                        Snackbar(message: item.message, type: item.type)
                            .transition(.move(edge: .top).combined(with: .opacity))
                            .animation(.spring())
                            .id(item.id)
                    }
                    .padding(.top)
                    Spacer()
                }
            )
    }
}

Доступность для VoiceOver



Не забывайте о пользователях с ограниченными возможностями. Сделаем наш попап доступным для VoiceOver:

Swift Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
extension View {
    func accessiblePopup<T: View>(
        isPresented: Bool,
        alignment: Alignment = .center,
        direction: Popup<T>.Direction = .bottom,
        onDismiss: (() -> Void)? = nil,
        accessibilityLabel: String,
        accessibilityHint: String? = nil,
        accessibilityAnnouncement: String? = nil,
        @ViewBuilder content: () -> T
    ) -> some View {
        self.popup(isPresented: isPresented, alignment: alignment, direction: direction, onDismiss: onDismiss) {
            content()
                .accessibility(label: Text(accessibilityLabel))
                .if(accessibilityHint != nil) { view in
                    view.accessibility(hint: Text(accessibilityHint!))
                }
                .onAppear {
                    if let announcement = accessibilityAnnouncement {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                            UIAccessibility.post(notification: .announcement, argument: announcement)
                        }
                    }
                }
        }
    }
}
 
// Вспомогательное расширение
extension View {
    @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}
Эти примеры демонстрируют гибкость и мощь нашего попап-компонента. От простых уведомлений до сложных форм и интерактивных меню — всё это можно реализовать с помощью единого подхода. Дополнительные модификаторы и обёртки позволяют адаптировать базовый компонент под конкретные сценарии использования, сохраняя при этом единообразие анимаций и поведения.

запросы к api в swiftui и uikit
Прошу не бросаться гнилыми помидорами я только начал изучать разработку под ios. Вопрос такой: Отличаются ли написания запросов к rest api в...

swiftui foreach
Скажите пожалуйста где ошибка: import SwiftUI struct ContentView: View { var body: some View { ForEach(0 ... 5, id: \.self)...

Передать результат запроса в SwiftUI
Как передать полученные данные в SwiftUI, чтобы на эмуляторе их вывести а не в консоли? guard let url = URL(string:...

swiftui self
self.items = try container.decode(.self, forKey: .items) Скажите пожалуйста что означает запись &quot;.self&quot;. Что такое self в данном...

дизайн swiftui
Как делаются интерфейсы приложений типа duolingo на swiftui? Я имею ввиду графические элементы.

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

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

SwiftUI. ScrollView с DragGesture
Использую swiftUI. У меня экран (VStack) появляется снизу вверх. На VStack расположен scrollview, вертикальный. По техническому заданию, если...

всплывающее попап на Ajax
надо чтоб при получении юзером нового сообщения на страничке без её перезагрузки всплывало попап, так вот как бы это зделать?я зделал что сообщение...

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

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

Как передать из попап окна родителю переменную?
Привет знатокам JavaScript ! Что- то не получается передать переменную в родительское окно, делаю так в попапе: &lt;a...

Метки ios, mobile, mobiledev, popup, swiftui
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Async/await в TypeScript
run.dev 06.04.2025
Асинхронное программирование — это подход к разработке программного обеспечения, при котором операции выполняются независимо друг от друга. В отличие от синхронного выполнения, где каждая последующая. . .
Многопоточность в C#: Синхронизация потоков
UnmanagedCoder 06.04.2025
Многопоточное программирование стало неотъемлемой частью разработки современных приложений на C#. С появлением многоядерных процессоров возможность выполнять несколько задач параллельно значительно. . .
TypeScript: Классы и конструкторы
run.dev 06.04.2025
TypeScript, как статически типизированный язык, построенный на основе JavaScript, привнес в веб-разработку новый уровень надежности и структурированности кода. Одним из важнейших элементов этой. . .
Многопоточное программирование: Rust против C++
golander 06.04.2025
C++ существует уже несколько десятилетий и его поддержка параллелизма постепенно наращивалась со временем. Начиная с C++11, язык получил стандартную библиотеку для работы с потоками, а в последующих. . .
std::vector в C++: от основ к оптимизации производительности
NullReferenced 05.04.2025
Для многих программистов знакомство с std::vector происходит на ранних этапах изучения языка, но между базовым пониманием и подлинным мастерством лежит огромная дистанция. Контейнер std::vector. . .
Реляционная модель и правила Кодда: фундамент современных баз данных
Codd 05.04.2025
Конец 1960-х — начало 1970-х годов был периодом глубоких трансформаций в области хранения и обработки данных. На фоне растущих потребностей бизнеса и правительственных структур существовавшие на тот. . .
Асинхронные операции в Django с Celery
py-thonny 05.04.2025
Разработчики Django часто сталкиваются с проблемой, когда пользователь нажимает кнопку отправки формы и. . . ждёт. Секунды растягиваются в минуты, терпение иссякает, а интерфейс приложения замирает. . . .
Использование кэшей CPU: Максимальная производительность в Go
golander 05.04.2025
Разработчикам хорошо известно, что эффективность кода зависит не только от алгоритмов и структур данных, но и от того, насколько удачно программа взаимодействует с железом. Среди множества факторов,. . .
Создаем Telegram бот на TypeScript с grammY
run.dev 05.04.2025
Одна из его самых сильных сторон Telegram — это интеграция ботов прямо в экосистему приложения. В отличие от многих других платформ, он предоставляет разработчикам мощный API, позволяющий создавать. . .
Паттерны распределённых транзакций в Event-Driven микросервисах
ArchitectMsa 05.04.2025
Современные программные системы всё чаще проектируются как совокупность взаимодействующих микросервисов. И хотя такой подход даёт множество преимуществ — масштабируемость, гибкость, устойчивость к. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru
Выделить код Копировать код Сохранить код Нормальный размер Увеличенный размер