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

Разработка контекстных меню в iOS

Запись от mobDevWorks размещена 14.03.2025 в 08:21
Показов 818 Комментарии 0
Метки ios, mobile, swift, swiftui

Нажмите на изображение для увеличения
Название: 121e4edf-6c90-468f-ae5c-4c90d27a5d1f.jpg
Просмотров: 63
Размер:	122.9 Кб
ID:	10387
С приходом iOS 13 Apple представила новый API для контекстных меню, который полностью заменил предыдущую технологию 3D Touch peek & pop. Хотя многие разработчики и пользователи испытывают ностальгию по 3D Touch, новый UIContextMenu API оказался заметно универсальнее. Он работает на всех устройствах, включая iPad, где 3D Touch никогда не был доступен.

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

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

API контекстных меню в iOS состоит из нескольких ключевых компонентов:
UIContextMenuInteraction — основной класс для создания взаимодействия
UIContextMenuInteractionDelegate — протокол делегата для настройки меню
UIContextMenuConfiguration — конфигурация для настройки содержимого меню
UIMenu и UIAction — классы для создания самого меню и его действий

Интересной особенностью современных контекстных меню является возможность создания вложенных структур. Вы можете группировать связанные действия в подменю, что делает пользовательский опыт организованным даже при большом количестве опций. В отличие от многих других элементов пользовательского интерфейса, контекстные меню требуют минимальной дополнительной настройки для работы с таблицами (UITableView) и коллекциями (UICollectionView). Фреймворк UIKit обеспечивает встроенную поддержку через методы делегатов.

Основы реализации



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

Основа контекстного меню в iOS 13+ состоит из двух ключевых элементов:
1. UIContextMenuInteraction — класс, управляющий взаимодействием с меню.
2. UIContextMenuInteractionDelegate — протокол, необходимый для настройки меню.

Начнем с создания базового класса ViewController:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SimpleMenuViewController: UIViewController {
    private let menuView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Создаем синий квадрат
        menuView.backgroundColor = .systemBlue
        menuView.frame.size = CGSize(width: 100, height: 100)
        view.addSubview(menuView)
        
        // Добавляем взаимодействие с контекстным меню
        let interaction = UIContextMenuInteraction(delegate: self)
        menuView.addInteraction(interaction)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        menuView.center = view.center
    }
}
Теперь нам нужно реализовать протокол UIContextMenuInteractionDelegate. Этот протокол имеет один обязательный метод — contextMenuInteraction(_:configurationForMenuAtLocation:), который возвращает конфигурацию меню:

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
extension SimpleMenuViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(
        _ interaction: UIContextMenuInteraction,
        configurationForMenuAtLocation location: CGPoint
    ) -> UIContextMenuConfiguration? {
        
        // Создаем и возвращаем конфигурацию для меню
        return UIContextMenuConfiguration(
            identifier: nil,
            previewProvider: nil
        ) { [weak self] _ in
            // Создаем действия для меню
            let shareAction = UIAction(
                title: "Поделиться",
                image: UIImage(systemName: "square.and.arrow.up")
            ) { _ in
                // Код для реализации функции "поделиться"
                print("Пользователь выбрал 'Поделиться'")
            }
            
            let renameAction = UIAction(
                title: "Переименовать",
                image: UIImage(systemName: "pencil")
            ) { _ in
                // Код для переименования
                print("Пользователь выбрал 'Переименовать'")
            }
            
            let deleteAction = UIAction(
                title: "Удалить",
                image: UIImage(systemName: "trash"),
                attributes: .destructive
            ) { _ in
                // Код для удаления
                print("Пользователь выбрал 'Удалить'")
            }
            
            // Собираем меню из действий
            return UIMenu(title: "", children: [shareAction, renameAction, deleteAction])
        }
    }
}
Обратите внимание на ключевые элементы в этом коде:
UIContextMenuConfiguration принимает три параметра:
- identifier — идентификатор конфигурации (используется для передачи данных).
- previewProvider — функция, возвращающая содержимое для предпросмотра.
- actionProvider — функция, создающая меню с действиями.
UIAction — класс для создания отдельных действий в меню. Каждое действие имеет:
- title — текстовое название действия.
- image — иконка (используются SF Symbols).
- attributes — дополнительные атрибуты (например, .destructive для опасных действий).
- замыкание, которое выполняется при выборе действия.

UIMenu — контейнер для группировки действий. Принимает заголовок и массив дочерних элементов.
После добавления этого кода, при длительном нажатии на синий квадрат должно появиться меню с тремя опциями. Система автоматически добавит красивую анимацию появления и исчезновения.

Настройка gestureRecognizer для работы с контекстными меню



Обычно UIContextMenuInteraction распознает жесты автоматически, но иногда требуется более тонкая настройка. Например, вы можете захотеть изменить время долгого нажатия или совместить контекстное меню с другими распознавателями жестов.
Для этого можно использовать метод contextMenuInteraction(_:shouldBeginAt:):

Swift
1
2
3
4
5
6
7
8
9
10
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    shouldBeginAt location: CGPoint
) -> Bool {
    // Проверяем, можно ли запустить контекстное меню
    // Например, проверяем, не находится ли под точкой location другой интерактивный элемент
    
    // Если все условия выполнены, разрешаем показ меню
    return true
}
Этот метод позволяет динамически решать, показывать ли меню в зависимости от контекста.

Работа с UIContextMenuInteraction



Кроме основной конфигурации, UIContextMenuInteraction предлагает несколько методов для управления жизненным циклом меню:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    willDisplayMenuFor configuration: UIContextMenuConfiguration,
    animator: UIContextMenuInteractionAnimating?
) {
    // Код выполнится непосредственно перед отображением меню
    // Можно использовать для подготовки интерфейса
}
 
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    willEndFor configuration: UIContextMenuConfiguration,
    animator: UIContextMenuInteractionAnimating?
) {
    // Код выполнится перед закрытием меню
    // Полезно для отмены временных изменений интерфейса
}
При разработке приложений с контекстными меню важно учитывать следующее:

1. Скорость работы — убедитесь, что код в previewProvider и actionProvider работает быстро, чтобы не тормозить появление меню.
2. Определение контекста — часто нужно определять, к какому элементу относится нажатие. В сложных интерфейсах можно использовать identifier для хранения этой информации.
3. Управление состоянием — если меню изменяет состояние приложения, убедитесь, что эти изменения корректно отражаются в интерфейсе после закрытия меню.

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

Обработка взаимодействий



Контекстные меню в iOS – это не просто способ показать пользователю набор действий. Это целая система взаимодействий, которая включает в себя показ меню, предпросмотр контента и переход к детальному представлению. Разберём подробнее, как можно обрабатывать эти взаимодействия.
Когда пользователь выбирает элемент из контекстного меню, срабатывает замыкание, связанное с соответствующим действием UIAction. Однако что происходит, когда пользователь нажимает на сам предпросмотр? По умолчанию ничего не происходит, но мы можем это изменить:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction, 
    willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, 
    animator: UIContextMenuInteractionCommitAnimating
) {
    // Этот код выполняется, когда пользователь нажимает на предпросмотр
    
    animator.addCompletion {
        // Здесь мы можем выполнить переход к детальному представлению
        if let viewController = animator.previewViewController {
            self.show(viewController, sender: self)
        }
    }
}
Этот метод позволяет реализовать поведение, похожее на старый механизм peek & pop: пользователь сначала видит предпросмотр, а затем может перейти к полному представлению контента.
При работе с контекстными меню часто требуется передавать данные между различными этапами взаимодействия. Для этого можно использовать поле identifier в UIContextMenuConfiguration:

Swift
1
2
3
4
5
6
let itemID = "unique-item-id-123" as NSString
let config = UIContextMenuConfiguration(
    identifier: itemID,
    previewProvider: { ... },
    actionProvider: { ... }
)
Этот идентификатор будет доступен во всех методах делегата, связанных с данной конфигурацией. Например, в методе willPerformPreviewActionForMenuWith:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction, 
    willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, 
    animator: UIContextMenuInteractionCommitAnimating
) {
    // Получаем идентификатор из конфигурации
    if let identifier = configuration.identifier as? String {
        let detailVC = DetailViewController(itemID: identifier)
        animator.addCompletion {
            self.show(detailVC, sender: self)
        }
    }
}
Это особенно удобно в случаях, когда вы не хотите использовать previewViewController для перехода, а предпочитаете создать новый контроллер с нужными параметрами.

Обработка отмены взаимодействия с контекстным меню



Иногда пользователь может начать взаимодействие с контекстным меню, но затем решить отменить его. В таких случаях полезно реагировать на отмену, чтобы восстановить предыдущее состояние интерфейса или выполнить другие необходимые действия. Для обработки отмены взаимодействия можно использовать метод contextMenuInteraction(_:willEndFor:animator:):

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction, 
    willEndFor configuration: UIContextMenuConfiguration, 
    animator: UIContextMenuInteractionAnimating?
) {
    // Выполняется перед закрытием меню
    
    // Можно добавить собственную анимацию закрытия
    animator?.addAnimations {
        // Код анимации, например:
        self.menuView.alpha = 1.0
    }
    
    // Или выполнить действия после закрытия меню
    animator?.addCompletion {
        // Убираем выделение элемента и т.п.
        self.menuView.layer.borderWidth = 0
    }
}
Этот метод дает возможность красиво завершить взаимодействие с меню, вне зависимости от того, выбрал ли пользователь действие или просто отменил меню. Рассмотрим еще один полезный сценарий - кастомизация поведения при длительном нажатии. По умолчанию контекстное меню появляется после стандартной задержки, но иногда требуется изменить это поведение:

Swift
1
2
3
4
5
6
7
8
9
10
// Для более тонкой настройки распознавания жеста
// можно получить доступ к распознавателю жестов:
if let interactions = view.interactions as? [UIContextMenuInteraction],
   let interaction = interactions.first {
    // Получаем распознаватель жестов
    if let gestureRecognizer = interaction.value(forKey: "_gestureRecognizer") as? UILongPressGestureRecognizer {
        // Настраиваем его параметры
        gestureRecognizer.minimumPressDuration = 0.3 // Уменьшаем время нажатия
    }
}
Обратите внимание: это хак, использующий приватное API, поэтому в продакшн-приложениях стоит избегать такого подхода. Но для создания прототипов и экспериментов это может быть полезно.

При реализации контекстных меню важно помнить о производительности. Методы делегата вызываются в основном потоке, поэтому любая блокирующая операция в них может привести к задержке отображения меню. Рекомендуется:
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
override func tableView(
    _ tableView: UITableView, 
    contextMenuConfigurationForRowAt indexPath: IndexPath, 
    point: CGPoint
) -> UIContextMenuConfiguration? {
    // Сразу получаем модель из текущего состояния таблицы
    guard let item = dataSource.itemIdentifier(for: indexPath) else {
        return nil
    }
    
    // Создаем копию или сохраняем идентификатор
    let itemCopy = item.copy() // или item.id
    
    return UIContextMenuConfiguration(
        identifier: itemCopy as NSCopying,
        previewProvider: { ... },
        actionProvider: { _ in
            // Используем itemCopy, а не обращаемся к dataSource по indexPath
            // так как indexPath может измениться к моменту выбора действия
        }
    )
}
Такой подход обеспечивает стабильную работу контекстных меню даже при динамических изменениях данных в вашем приложении.

Разработка под iOS
Привет С чего начать разработку под iOS? Имею ввиду, что для этого нужно знать, что учить? Какие языки надо знать? Какие инструменты для...

Ios 8.x в iphone 4s или оставить ios 7.x?
стоит ли перепрошивать 4s или остаться на семерке.... думаю что 8ка будет работать медленне?

IOS 7
Добрый день, рад что зашел на ваш форум, надеюсь что мы найдем общий язык! Я официальный разработчик Apple ios DEV CENTER, уже более 3-х лет. ...

Редактирование контекстных меню через реестр
В каких ветках реестра храняться контекстные меню для разных объектов? файл/папка/окно и другие в часности интересоет меню которое вызываеться в...


Продвинутые техники



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

Настройка анимации предпросмотра



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

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
// Настраивает анимацию появления предпросмотра
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
    // Создаем настраиваемый предпросмотр
    guard let targetView = menuView else { return nil }
    
    // Настраиваем параметры предпросмотра
    let params = UIPreviewParameters()
    params.backgroundColor = .clear
    
    // Можно задать путь для маски предпросмотра
    let roundPath = UIBezierPath(roundedRect: targetView.bounds, 
                                cornerRadius: 15)
    params.visiblePath = roundPath
    
    // Возвращаем настроенный предпросмотр
    return UITargetedPreview(view: targetView, parameters: params)
}
 
// Настраивает анимацию исчезновения предпросмотра
func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
    // Обычно возвращаем тот же предпросмотр, что и для появления
    return contextMenuInteraction(
        interaction,
        previewForHighlightingMenuWithConfiguration: configuration
    )
}
С помощью этих методов мы можем контролировать, как выглядит исходный элемент при появлении меню. Например, можно задать закругленные углы или изменить цвет фона только для предпросмотра, не меняя сам исходный элемент.

Кастомные контроллеры предпросмотра



Еще более мощная возможность — создание полностью кастомного контроллера для предпросмотра. Для этого используется previewProvider в конфигурации меню:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
return UIContextMenuConfiguration(
    identifier: nil,
    previewProvider: {
        // Создаем и возвращаем контроллер для предпросмотра
        let previewController = CustomPreviewController()
        
        // Можно настроить размер предпросмотра
        let image = UIImage(named: "preview-image")!
        let width = UIScreen.main.bounds.width * 0.7
        let aspectRatio = image.size.height / image.size.width
        let height = width * aspectRatio
        
        previewController.preferredContentSize = CGSize(width: width, height: height)
        
        return previewController
    },
    actionProvider: { ... }
)
Класс CustomPreviewController — это обычный UIViewController, который может содержать любую логику и UI. Особенность в том, что он будет использоваться системой для отображения предпросмотра. Важно помнить о preferredContentSize — этот параметр контролирует размер предпросмотра. Особенно полезно устанавливать его при работе с изображениями, чтобы сохранить правильное соотношение сторон.

Вложенные меню



Одна из самых полезных функций контекстных меню в iOS 13+ — возможность создавать вложенные меню. Это позволяет организовать большое количество опций в иерархическую структуру, делая интерфейс более понятным и менее перегруженным. Создание вложенного меню не сложнее, чем создание обычного. Нам просто нужно включить один UIMenu как дочерний элемент другого:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Создаем подменю для опций форматирования
let boldAction = UIAction(title: "Полужирный", image: UIImage(systemName: "bold")) { _ in }
let italicAction = UIAction(title: "Курсив", image: UIImage(systemName: "italic")) { _ in }
let underlineAction = UIAction(title: "Подчеркнутый", image: UIImage(systemName: "underline")) { _ in }
 
// Группируем действия в подменю
let formatMenu = UIMenu(
    title: "Форматирование",
    image: UIImage(systemName: "textformat"),
    children: [boldAction, italicAction, underlineAction]
)
 
// Создаем основное меню
let shareAction = UIAction(title: "Поделиться", image: UIImage(systemName: "square.and.arrow.up")) { _ in }
let deleteAction = UIAction(title: "Удалить", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in }
 
// Включаем подменю форматирования в основное меню
let mainMenu = UIMenu(title: "", children: [shareAction, formatMenu, deleteAction])
В этом примере мы создали меню "Форматирование", которое содержит три действия связанные с форматированием текста, и включили его в основное меню наряду с действиями "Поделиться" и "Удалить".

Умное разделение меню



Иногда нужно визуально разделить опции в меню без создания вложенных структур. Для этого можно использовать параметр options при создании UIMenu:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Создаем группу опций редактирования
let copyAction = UIAction(title: "Копировать", image: UIImage(systemName: "doc.on.doc")) { _ in }
let cutAction = UIAction(title: "Вырезать", image: UIImage(systemName: "scissors")) { _ in }
let pasteAction = UIAction(title: "Вставить", image: UIImage(systemName: "doc.on.clipboard")) { _ in }
 
// Создаем меню с опцией displayInline
let editMenu = UIMenu(
    title: "Редактирование",
    options: .displayInline,
    children: [copyAction, cutAction, pasteAction]
)
 
// Создаем другие действия
let shareAction = UIAction(title: "Поделиться", image: UIImage(systemName: "square.and.arrow.up")) { _ in }
let deleteAction = UIAction(title: "Удалить", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in }
 
// Собираем все в основное меню
let mainMenu = UIMenu(title: "", children: [shareAction, editMenu, deleteAction])
Опция .displayInline указывает системе отображать дочерние элементы меню на том же уровне, что и другие элементы основного меню, но с визуальным разделителем. Это создает логическую группировку без необходимости дополнительного нажатия для открытия подменю.

Деструктивные действия и подтверждение



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

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Создаем действие отмены
let cancelAction = UIAction(title: "Отмена", image: UIImage(systemName: "xmark")) { _ in }
 
// Создаем действие подтверждения удаления
let confirmDeleteAction = UIAction(
    title: "Подтвердить удаление",
    image: UIImage(systemName: "checkmark"),
    attributes: .destructive
) { _ in
    // Выполняем удаление
    print("Элемент удален")
}
 
// Создаем подменю для подтверждения
let deleteConfirmMenu = UIMenu(
    title: "Удалить?",
    options: .destructive,
    children: [cancelAction, confirmDeleteAction]
)
 
// Добавляем это меню в основное меню
let mainMenu = UIMenu(title: "", children: [/* другие действия */, deleteConfirmMenu])
В этом примере пользователю придется сначала выбрать "Удалить?", а затем "Подтвердить удаление", чтобы выполнить действие. Это снижает вероятность случайного удаления контента. Опция .destructive при создании UIMenu придает всему меню деструктивный вид (обычно красный цвет), что дополнительно сигнализирует пользователю о потенциально опасном действии.

Кастомизация визуального отображения меню



Иногда стандартного внешнего вида меню может быть недостаточно для создания уникального пользовательского опыта. Хотя iOS не предоставляет прямых API для изменения внешнего вида самого меню, существуют различные приёмы, позволяющие влиять на то, как оно воспринимается пользователем. Одна из таких возможностей — настройка заголовков меню. Заголовки можно использовать для добавления контекста, когда действия могут быть неоднозначными:

Swift
1
2
3
4
5
6
7
8
let exportMenu = UIMenu(
    title: "Выберите формат экспорта:",
    children: [
        UIAction(title: "PDF", image: UIImage(systemName: "doc.pdf")) { _ in },
        UIAction(title: "JPEG", image: UIImage(systemName: "photo")) { _ in },
        UIAction(title: "PNG", image: UIImage(systemName: "photo.fill")) { _ in }
    ]
)
Заголовок "Выберите формат экспорта:" делает меню более понятным, особенно если оно глубоко вложено. Другой важный аспект — выбор подходящих иконок. SF Symbols предлагает богатую библиотеку согласованных иконок, которые отлично работают с контекстными меню:

Swift
1
2
3
4
5
6
7
8
// Использование вариативных символов, которые могут изменять своё внешнее представление
let favoriteAction = UIAction(
    title: "Добавить в избранное", 
    image: UIImage(systemName: "star.fill"), 
    state: isFavorited ? .on : .off
) { action in
    self.toggleFavorite()
}
Обратите внимание на параметр state, который позволяет отобразить текущее состояние переключаемой опции. Это мощный инструмент для создания контекстно-зависимых меню.

Работа с UITargetedPreview для продвинутой настройки предпросмотра



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

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
    
    // Получаем идентификатор для определения конкретной ячейки
    guard let identifier = configuration.identifier as? String,
          let row = dataItems.firstIndex(where: { $0.id == identifier }),
          let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) as? CustomCell else {
        return nil
    }
    
    // Настраиваем параметры предпросмотра
    let parameters = UIPreviewParameters()
    parameters.backgroundColor = .clear
    
    // Если иконка круглая, создаём круглую маску
    let iconFrame = cell.iconView.bounds
    parameters.visiblePath = UIBezierPath(ovalIn: iconFrame)
    
    // Создаём и возвращаем таргетированный предпросмотр для иконки
    return UITargetedPreview(view: cell.iconView, parameters: parameters)
}
В этом примере мы создаем предпросмотр только для iconView внутри ячейки, а не для всей ячейки. Параметр visiblePath позволяет определить форму предпросмотра, в данном случае — круг, который соответствует круглой иконке. Не забудьте также реализовать метод для завершения предпросмотра:

Swift
1
2
3
4
override func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
    // Обычно здесь используется тот же код, что и в методе выше
    return tableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
}
Интересно, что можно комбинировать кастомный UITargetedPreview с использованием previewProvider. В этом случае анимация будет происходить между целевым представлением (заданным через UITargetedPreview) и контроллером предпросмотра (заданным через previewProvider).

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    
    let item = dataItems[indexPath.item]
    let identifier = item.id as NSString
    
    return UIContextMenuConfiguration(
        identifier: identifier,
        previewProvider: {
            // Создаём контроллер для полного предпросмотра
            return DetailPreviewController(item: item)
        },
        actionProvider: { _ in
            return self.makeMenuForItem(item)
        }
    )
}
Такой подход создаёт плавную и приятную анимацию между исходным элементом (например, миниатюрой изображения) и полным предпросмотром.

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



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

Контекстные меню для изображений



Фотоприложения — идеальные кандидаты для использования контекстных меню. Они удобны для быстрого доступа к распространенным действиям с изображениями без необходимости открывать отдельные экраны.

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
69
70
71
72
73
74
75
76
77
78
79
80
81
class PhotoCollectionViewController: UICollectionViewController {
    
    private var photos: [Photo] = []
    
    // Настройка коллекции фотографий
    
    override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        
        let photo = photos[indexPath.item]
        
        return UIContextMenuConfiguration(
            identifier: photo.id as NSString,
            previewProvider: {
                return PhotoPreviewViewController(photo: photo)
            },
            actionProvider: { _ in
                
                // Действие для просмотра фото
                let viewAction = UIAction(
                    title: "Просмотр", 
                    image: UIImage(systemName: "eye")
                ) { _ in
                    self.showFullscreen(photo: photo)
                }
                
                // Действие для добавления в альбом
                let albumAction = UIAction(
                    title: "Добавить в альбом",
                    image: UIImage(systemName: "rectangle.stack")
                ) { _ in
                    self.addToAlbum(photo: photo)
                }
                
                // Действия для редактирования
                let cropAction = UIAction(title: "Обрезать", image: UIImage(systemName: "crop")) { _ in 
                    self.cropPhoto(photo: photo)
                }
                
                let filtersAction = UIAction(title: "Фильтры", image: UIImage(systemName: "camera.filters")) { _ in
                    self.applyFilters(photo: photo)
                }
                
                // Группируем действия редактирования
                let editMenu = UIMenu(
                    title: "Редактировать",
                    image: UIImage(systemName: "wand.and.stars"),
                    children: [cropAction, filtersAction]
                )
                
                // Действие для удаления
                let deleteAction = UIAction(
                    title: "Удалить",
                    image: UIImage(systemName: "trash"),
                    attributes: .destructive
                ) { _ in
                    self.deletePhoto(photo: photo)
                }
                
                // Формируем итоговое меню
                return UIMenu(
                    title: "",
                    children: [viewAction, albumAction, editMenu, deleteAction]
                )
            }
        )
    }
    
    // Реализация анимированного перехода при нажатии на предпросмотр
    override func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        
        animator.addCompletion {
            if let photoID = configuration.identifier as? String,
               let photo = self.photos.first(where: { $0.id == photoID }) {
                let detailVC = PhotoDetailViewController(photo: photo)
                self.present(detailVC, animated: true)
            }
        }
    }
    
    // Другие методы для управления фотографиями...
}
В этом примере мы создали меню для работы с фотографиями, включая просмотр, редактирование и удаление. Для редактирования мы использовали вложенное меню, чтобы сгруппировать связанные действия.

Реализация контекстных меню в SwiftUI



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

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
struct PhotoView: View {
    let photo: Photo
    @State private var isFavorite = false
    
    var body: some View {
        Image(uiImage: photo.image)
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: 150, height: 150)
            .cornerRadius(8)
            .contextMenu {
                Button(action: {
                    // Поделиться фотографией
                }) {
                    Label("Поделиться", systemImage: "square.and.arrow.up")
                }
                
                Button(action: {
                    self.isFavorite.toggle()
                }) {
                    Label(
                        isFavorite ? "Удалить из избранного" : "Добавить в избранное",
                        systemImage: isFavorite ? "star.fill" : "star"
                    )
                }
                
                Button(action: {
                    // Редактирование
                }) {
                    Label("Редактировать", systemImage: "slider.horizontal.3")
                }
                
                Button(role: .destructive, action: {
                    // Удалить фото
                }) {
                    Label("Удалить", systemImage: "trash")
                }
            }
    }
}
В SwiftUI мы используем кнопки (Button) вместо UIAction, а для создания иерархии можно использовать группы:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.contextMenu {
    Button(action: { /* ... */ }) {
        Label("Просмотр", systemImage: "eye")
    }
    
    Group {
        Text("Редактирование")
        
        Button(action: { /* ... */ }) {
            Label("Обрезать", systemImage: "crop")
        }
        
        Button(action: { /* ... */ }) {
            Label("Применить фильтры", systemImage: "camera.filters")
        }
    }
}
Стоит отметить, что поддержка глубоко вложенных подменю в SwiftUI пока ограничена, но основные возможности доступны.

Работа с таблицами и коллекциями



Контекстные меню отлично работают с UITableView и UICollectionView. Фреймворк предоставляет специализированные методы делегатов для добавления меню к ячейкам, а также параметры для создания кастомных предпросмотров. Пример добавления контекстного меню в список контактов:

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
69
70
71
72
73
74
75
76
77
78
79
class ContactsTableViewController: UITableViewController {
    
    var contacts: [Contact] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Настройка таблицы...
    }
    
    override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        
        let contact = contacts[indexPath.row]
        
        return UIContextMenuConfiguration(identifier: contact.id as NSString) { 
            // Preview provider - можем создать контроллер для предпросмотра контакта
            let previewController = ContactPreviewController(contact: contact)
            return previewController
            
        } actionProvider: { _ in
            
            // Действие для звонка
            let callAction = UIAction(title: "Позвонить", image: UIImage(systemName: "phone")) { _ in
                self.callContact(contact)
            }
            
            // Действие для сообщения
            let messageAction = UIAction(title: "Сообщение", image: UIImage(systemName: "message")) { _ in
                self.messageContact(contact)
            }
            
            // Действие для видеозвонка
            let videoCallAction = UIAction(title: "FaceTime", image: UIImage(systemName: "video")) { _ in
                self.videoCallContact(contact)
            }
            
            // Группируем действия коммуникации
            let communicationMenu = UIMenu(title: "Связаться", children: [callAction, messageAction, videoCallAction])
            
            // Действие для добавления в избранное
            let favoriteAction = UIAction(
                title: contact.isFavorite ? "Удалить из избранного" : "Добавить в избранное",
                image: UIImage(systemName: contact.isFavorite ? "star.fill" : "star")
            ) { _ in
                self.toggleFavorite(contact)
            }
            
            // Действие для редактирования
            let editAction = UIAction(title: "Редактировать", image: UIImage(systemName: "pencil")) { _ in
                self.editContact(contact)
            }
            
            // Действие для удаления
            let deleteAction = UIAction(
                title: "Удалить",
                image: UIImage(systemName: "trash"),
                attributes: .destructive
            ) { _ in
                self.deleteContact(contact)
            }
            
            // Собираем меню
            return UIMenu(title: "", children: [communicationMenu, favoriteAction, editAction, deleteAction])
        }
    }
    
    // Реализация перехода при нажатии на предпросмотр
    override func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        
        animator.addCompletion {
            if let contactID = configuration.identifier as? String,
               let contact = self.contacts.first(where: { $0.id == contactID }) {
                let detailVC = ContactDetailViewController(contact: contact)
                self.show(detailVC, sender: self)
            }
        }
    }
    
    // Методы для работы с контактами...
}

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



API UIContextMenuConfiguration можно использовать и в других интересных сценариях. Рассмотрим пример для списка задач:

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
69
70
71
72
class TasksViewController: UITableViewController {
    
    var tasks: [Task] = []
    
    override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        
        let task = tasks[indexPath.row]
        
        // Создаём динамические действия в зависимости от состояния задачи
        return UIContextMenuConfiguration(identifier: task.id as NSString) { 
            return TaskPreviewController(task: task)
            
        } actionProvider: { _ in
            
            // Динамически создаём действие в зависимости от статуса задачи
            let statusAction: UIAction
            
            switch task.status {
            case .todo:
                statusAction = UIAction(title: "Начать выполнение", image: UIImage(systemName: "play")) { _ in
                    self.startTask(task)
                }
            case .inProgress:
                statusAction = UIAction(title: "Завершить", image: UIImage(systemName: "checkmark.circle")) { _ in
                    self.completeTask(task)
                }
            case .completed:
                statusAction = UIAction(title: "Отметить как незавершённую", image: UIImage(systemName: "arrow.counterclockwise")) { _ in
                    self.reopenTask(task)
                }
            }
            
            // Действие для установки приоритета
            let lowPriorityAction = UIAction(
                title: "Низкий",
                image: UIImage(systemName: "arrow.down.circle"),
                state: task.priority == .low ? .on : .off
            ) { _ in
                self.setTaskPriority(task, priority: .low)
            }
            
            let mediumPriorityAction = UIAction(
                title: "Средний",
                image: UIImage(systemName: "circle"),
                state: task.priority == .medium ? .on : .off
            ) { _ in
                self.setTaskPriority(task, priority: .medium)
            }
            
            let highPriorityAction = UIAction(
                title: "Высокий",
                image: UIImage(systemName: "exclamationmark.circle"),
                state: task.priority == .high ? .on : .off
            ) { _ in
                self.setTaskPriority(task, priority: .high)
            }
            
            // Создаём подменю приоритетов
            let priorityMenu = UIMenu(
                title: "Приоритет",
                image: UIImage(systemName: "flag"),
                options: .displayInline,
                children: [lowPriorityAction, mediumPriorityAction, highPriorityAction]
            )
            
            // Другие действия и финальное меню
            // ...
            
            return UIMenu(title: "", children: [statusAction, priorityMenu, /* ... */])
        }
    }
}

Решение нестандартных задач



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

Контекстное меню с текущими данными



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

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func createStatisticsMenu() -> UIMenu {
    // Получаем актуальную статистику в момент открытия меню
    let stats = projectStatistics.getCurrentStats()
    
    // Создаем действия с включением данных в названия
    let completionAction = UIAction(
        title: "Прогресс: \(stats.completionPercentage)%",
        image: UIImage(systemName: "chart.bar")
    ) { _ in
        self.showCompletionDetails()
    }
    
    let hoursAction = UIAction(
        title: "Затрачено: \(stats.hoursSpent) ч",
        image: UIImage(systemName: "clock")
    ) { _ in
        self.showTimeDetails()
    }
    
    return UIMenu(title: "Статистика", children: [completionAction, hoursAction])
}
Такой подход может быть полезен для проектных менеджеров, трекеров привычек или любых приложений, где важна актуальная информация.

Адаптивные меню в зависимости от контекста



Еще один прием — изменение содержимого меню в зависимости от состояния приложения или пользовательских настроек:

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
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
    
    return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
        var actions: [UIMenuElement] = []
        
        // Базовые действия, доступные всегда
        actions.append(UIAction(title: "Просмотр", image: UIImage(systemName: "eye")) { _ in })
        
        // Добавляем редактирование только если пользователь имеет права
        if userPermissions.canEdit {
            actions.append(UIAction(title: "Редактировать", image: UIImage(systemName: "pencil")) { _ in })
        }
        
        // Добавляем экспорт только для премиум-пользователей
        if userSubscription.isPremium {
            let pdfAction = UIAction(title: "PDF", image: UIImage(systemName: "doc.pdf")) { _ in }
            let xlsAction = UIAction(title: "Excel", image: UIImage(systemName: "doc.text")) { _ in }
            
            actions.append(UIMenu(title: "Экспорт", children: [pdfAction, xlsAction]))
        }
        
        // Только администраторы могут удалять
        if userPermissions.isAdmin {
            actions.append(UIAction(
                title: "Удалить", 
                image: UIImage(systemName: "trash"),
                attributes: .destructive
            ) { _ in })
        }
        
        return UIMenu(title: "", children: actions)
    }
}

Интеграция с системными функциями



Контекстные меню можно интегрировать с системными функциями iOS, например с распознаванием текста (OCR) или распознаванием объектов на изображениях:

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
func createPhotoAnalysisMenu(for photo: UIImage) -> UIMenu {
    var children: [UIMenuElement] = []
    
    // Базовые действия с фото
    children.append(UIAction(title: "Поделиться", image: UIImage(systemName: "square.and.arrow.up")) { _ in })
    
    // Проверяем, есть ли на фото текст для распознавания
    if ImageAnalyzer.containsText(photo) {
        children.append(UIAction(title: "Распознать текст", image: UIImage(systemName: "text.viewfinder")) { _ in
            self.recognizeText(in: photo)
        })
    }
    
    // Проверяем, есть ли на фото определяемые объекты
    let detectedObjects = ImageAnalyzer.detectObjects(in: photo)
    if !detectedObjects.isEmpty {
        let objectActions = detectedObjects.map { object in
            return UIAction(title: "Найти похожие \(object.name)", image: UIImage(systemName: "magnifyingglass")) { _ in
                self.searchSimilar(object: object)
            }
        }
        
        children.append(UIMenu(title: "Найдено на фото", children: objectActions))
    }
    
    return UIMenu(title: "", children: children)
}

Контекстные меню с индикацией состояния



Для задач, связанных с отслеживанием состояния, полезно использовать состояние действия (.on/`.off`):

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func createFilterMenu() -> UIMenu {
    let filters = FilterManager.available
    
    let filterActions = filters.map { filter in
        return UIAction(
            title: filter.name,
            image: UIImage(systemName: filter.iconName),
            state: activeFilters.contains(filter.id) ? .on : .off
        ) { [weak self] _ in
            self?.toggleFilter(filter.id)
        }
    }
    
    return UIMenu(title: "Фильтры", children: filterActions)
}
Такой подход создает удобный способ переключения множества опций без необходимости открывать отдельный экран настроек.

Лучшие практики и подводные камни



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

Оптимизация производительности



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

1. Подготавливайте данные заранее — не выполняйте тяжелые вычисления в методах делегата. Например, если ваше меню показывает статистику, вычислите её заранее или используйте кэширование:

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
// Плохо: тяжелые вычисления в методе делегата
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
    // Здесь происходит сложное вычисление статистики
    let stats = calculateExpensiveStatistics() 
    
    return UIContextMenuConfiguration(...) { _ in
        let action = UIAction(title: "Статистика: \(stats.value)") { _ in }
        return UIMenu(title: "", children: [action])
    }
}
 
// Хорошо: данные подготовлены заранее или кэшированы
private var cachedStats: Statistics?
private var statsLastUpdated: Date?
 
func updateCachedStatsIfNeeded() {
    // Обновляем кэш только если данные устарели
    if statsLastUpdated == nil || Date().timeIntervalSince(statsLastUpdated!) > 60 {
        cachedStats = calculateExpensiveStatistics()
        statsLastUpdated = Date()
    }
}
 
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
    return UIContextMenuConfiguration(...) { _ in
        let action = UIAction(title: "Статистика: \(self.cachedStats?.value ?? 0)") { _ in }
        return UIMenu(title: "", children: [action])
    }
}
2. Оптимизируйте предпросмотр — если вы создаете кастомный контроллер для предпросмотра, убедитесь, что он загружается быстро:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func previewProvider() -> UIViewController? {
    let vc = PreviewViewController()
    
    // Начинаем загрузку данных асинхронно
    DispatchQueue.global().async {
        let data = self.loadData()
        
        DispatchQueue.main.async {
            vc.updateUI(with: data)
        }
    }
    
    // Возвращаем контроллер немедленно с базовым UI
    // Детали подгрузятся позже
    return vc
}
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
class ImageCacheManager {
    static let shared = ImageCacheManager()
    private var cache = NSCache<NSString, UIImage>()
    
    func thumbnailForImage(id: String, completion: @escaping (UIImage?) -> Void) {
        let key = NSString(string: "thumb_\(id)")
        
        // Проверяем кэш сначала
        if let cached = cache.object(forKey: key) {
            completion(cached)
            return
        }
        
        // Асинхронно создаем миниатюру
        DispatchQueue.global().async {
            guard let fullImage = self.loadFullImage(id: id) else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            
            let thumbnail = fullImage.preparingThumbnail(of: CGSize(width: 300, height: 300))
            
            DispatchQueue.main.async {
                if let thumb = thumbnail {
                    self.cache.setObject(thumb, forKey: key)
                }
                completion(thumbnail)
            }
        }
    }
}

Сочетание с другими UI элементами



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

1. Конфликты с другими распознавателями жестов — если у вас есть другие жесты на том же view, вы можете столкнуться с конфликтами. Решение — правильная настройка приоритетов:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
view.addGestureRecognizer(panGesture)
 
// Найдем распознаватель контекстного меню
if let interactions = view.interactions as? [UIContextMenuInteraction],
   let menuInteraction = interactions.first,
   let menuGesture = menuInteraction.value(forKey: "_gestureRecognizer") as? UIGestureRecognizer {
    
    // Установим зависимость, чтобы panGesture не срабатывал
    // если начало распознаваться контекстное меню
    panGesture.require(toFail: menuGesture)
}
2. Интеграция с UITableView свайпами — если ваша таблица поддерживает свайп-действия, контекстные меню могут конфликтовать с ними. Возможное решение — отключить контекстные меню для ячеек со свайпом или настроить их совместную работу:

Swift
1
2
3
4
5
6
7
8
9
10
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    let item = items[indexPath.row]
    
    // Не показываем контекстное меню для элементов, где предпочтительнее свайп
    if item.preferSwipeActions {
        return nil
    }
    
    return UIContextMenuConfiguration(...)
}
3. Работа с UIScrollView — иногда контекстные меню могут мешать прокрутке. Убедитесь, что у пользователя есть возможность прокручивать контент без случайного вызова меню:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // Если пользователь активно прокручивает, временно отключаем контекстные меню
    if abs(scrollView.panGestureRecognizer.velocity(in: scrollView).y) > 500 {
        disableContextMenusTemporarily()
    }
}
 
func disableContextMenusTemporarily() {
    // Отключаем на короткий промежуток времени
    contextMenusEnabled = false
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
        self.contextMenusEnabled = true
    }
}
 
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    guard contextMenusEnabled else { return nil }
    
    // Обычная конфигурация меню
}

Тестирование на разных устройствах



Контекстные меню могут по-разному выглядеть и работать на различных устройствах. Вот на что стоит обратить внимание:

1. Различия между iPhone и iPad — на iPad контекстные меню отображаются как всплывающие окна, а на iPhone они занимают весь экран. Адаптируйте свой дизайн соответственно:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func configurePreviewController() -> UIViewController {
    let previewVC = PreviewViewController()
    
    // Определяем, запущено ли приложение на iPad
    let isIpad = UIDevice.current.userInterfaceIdiom == .pad
    
    if isIpad {
        // Для iPad делаем предпросмотр больше, так как он появляется в попапе
        previewVC.preferredContentSize = CGSize(width: 400, height: 400)
    } else {
        // Для iPhone учитываем доступную площадь экрана
        let width = UIScreen.main.bounds.width
        previewVC.preferredContentSize = CGSize(width: width, height: width)
    }
    
    return previewVC
}
2. Адаптация к размерам экрана — на устройствах с маленьким экраном (например, iPhone SE) меню может быть обрезано, если оно содержит слишком много элементов. Рассмотрите возможность сокращения количества элементов для таких устройств:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
    
    return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
        var actions: [UIMenuElement] = []
        
        // Базовые критически важные действия
        actions.append(UIAction(title: "Просмотр", image: UIImage(systemName: "eye")) { _ in })
        actions.append(UIAction(title: "Поделиться", image: UIImage(systemName: "square.and.arrow.up")) { _ in })
        
        // Дополнительные действия только для больших экранов
        let isSmallScreen = UIScreen.main.bounds.width < 375
        if !isSmallScreen {
            actions.append(UIAction(title: "Добавить в избранное", image: UIImage(systemName: "star")) { _ in })
            actions.append(UIAction(title: "Отправить другу", image: UIImage(systemName: "person.crop.circle")) { _ in })
        }
        
        return UIMenu(title: "", children: actions)
    }
}
3. Тестирование на устройствах с разной производительностью — на более старых устройствах анимация может работать не так плавно. Убедитесь, что ваше меню работает хорошо на всех поддерживаемых устройствах:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
// Упрощенный предпросмотр для старых устройств
func createPreviewController() -> UIViewController {
    // Проверяем производительность устройства
    let isLowPowerDevice = ProcessInfo.processInfo.thermalState == .serious || ProcessInfo.processInfo.thermalState == .critical
    
    if isLowPowerDevice {
        // Используем упрощенный предпросмотр
        return SimplifiedPreviewController(item: currentItem)
    } else {
        // Используем полный предпросмотр с анимациями
        return FullPreviewController(item: currentItem)
    }
}

Различия реализации контекстных меню между iPadOS и iOS



Хотя базовый API для контекстных меню одинаков для iOS и iPadOS, есть несколько важных различий, которые стоит учитывать:

1. Визуальное представление — на iPad меню отображаются в виде всплывающих окон, а не полноэкранных перекрытий, как на iPhone:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
func adaptPreviewForCurrentDevice(previewVC: UIViewController) {
    if UIDevice.current.userInterfaceIdiom == .pad {
        // На iPad предпросмотр выглядит лучше с закругленными углами
        previewVC.view.layer.cornerRadius = 12
        previewVC.view.layer.masksToBounds = true
        
        // Предпросмотры на iPad обычно меньше по размеру
        // и адаптированы для всплывающего отображения
        let width = min(540, UIScreen.main.bounds.width * 0.5)
        let height = width * 0.75
        previewVC.preferredContentSize = CGSize(width: width, height: height)
    }
}
2. Поддержка мыши и трекпада — на iPad с iPadOS 13.4+ пользователи могут вызывать контекстные меню также с помощью правой кнопки мыши или двумя пальцами на трекпаде:

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
// Этот метод позволяет определить, как было вызвано меню
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
    
    // Получаем текущий тип ввода
    let currentInput = UIDevice.current.userInterfaceIdiom
    
    animator.addCompletion {
        // Адаптируем переход в зависимости от типа ввода
        if currentInput == .pad {
            // Проверяем, использовалась ли мышь
            let mouseConnected = GCMouse.current != nil
            
            if mouseConnected {
                // Специальная анимация для перехода при использовании мыши
                self.presentDetailWithMouseAnimation()
            } else {
                self.presentDetailWithTouchAnimation()
            }
        } else {
            // Стандартный переход для iPhone
            self.presentDetail()
        }
    }
}
3. Курсор и наведение — на iPad с подключенной мышью или трекпадом контекстные меню также могут реагировать на эффект наведения:

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
// Если вы хотите добавить подсказку при наведении курсора
override func viewDidLoad() {
    super.viewDidLoad()
    
    // Настраиваем интеракцию для контекстного меню
    let interaction = UIContextMenuInteraction(delegate: self)
    menuView.addInteraction(interaction)
    
    // Добавляем поддержку наведения курсора
    menuView.addInteraction(UIHoverGestureRecognizer(target: self, action: #selector(handleHover(_:))))
}
 
@objc func handleHover(_ recognizer: UIHoverGestureRecognizer) {
    switch recognizer.state {
    case .began, .changed:
        // Показываем подсказку при наведении
        menuView.backgroundColor = .systemBlue.withAlphaComponent(0.8)
        
        // Можно также показать лейбл с подсказкой
        hintLabel.isHidden = false
        
    case .ended, .cancelled:
        // Скрываем подсказку
        menuView.backgroundColor = .systemBlue
        hintLabel.isHidden = true
        
    default:
        break
    }
}
4. Режим Split View — на iPad пользователи могут использовать ваше приложение в режиме разделенного экрана, что влияет на пространство, доступное для контекстных меню:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    
    // Адаптируемся к изменению размера окна (например, при переходе в Split View)
    coordinator.animate(alongsideTransition: { _ in
        // Обновляем макет при изменении размера
        self.updateLayoutForCurrentSize(size)
    })
}
 
func updateLayoutForCurrentSize(_ size: CGSize) {
    // Проверяем, запущены ли мы в компактном режиме (узкий Split View)
    let isCompact = size.width < 600
    
    if isCompact {
        // Используем более компактное представление
        previewSize = CGSize(width: size.width * 0.8, height: size.width * 0.6)
    } else {
        // Используем стандартное представление
        previewSize = CGSize(width: 400, height: 300)
    }
}
5. Интеграция с macOS через Catalyst — если ваше приложение работает также на macOS через Catalyst, контекстные меню будут адаптированы к нативным меню macOS:

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
#if targetEnvironment(macCatalyst)
// Когда приложение работает на Mac через Catalyst
func adaptMenuForMacCatalyst() -> UIMenu {
    // На macOS контекстные меню не поддерживают изображения
    // и аналогичны нативным контекстным меню macOS
    
    let viewAction = UIAction(title: "Просмотреть", image: nil) { _ in }
    let shareAction = UIAction(title: "Поделиться", image: nil) { _ in }
    
    // На Mac удобно добавить сочетания клавиш
    let editAction = UIAction(
        title: "Редактировать",
        image: nil,
        attributes: [],
        state: .off,
        handler: { _ in }
    )
    
    // Для macOS можно добавить разделитель
    let menu = UIMenu(title: "", options: [], children: [viewAction, shareAction, editAction])
    
    return menu
}
#endif

Подводные камни и способы их обхода



При работе с контекстными меню в iOS есть несколько неочевидных моментов, о которых стоит знать:

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

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
// Проблема: индекс может измениться пока меню открыто
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    // Используем indexPath напрямую в действиях - это опасно!
    return UIContextMenuConfiguration(...) { _ in
        return UIMenu(title: "", children: [
            UIAction(title: "Удалить") { _ in
                // К этому моменту indexPath может указывать на другой элемент!
                self.items.remove(at: indexPath.row)
                self.tableView.deleteRows(at: [indexPath], with: .automatic)
            }
        ])
    }
}
 
// Решение: сохраняем модель или идентификатор вместо индекса
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    let item = items[indexPath.row]
    let itemID = item.id // Уникальный идентификатор
    
    return UIContextMenuConfiguration(identifier: itemID as NSCopying, previewProvider: nil) { _ in
        return UIMenu(title: "", children: [
            UIAction(title: "Удалить") { [weak self] _ in
                guard let self = self else { return }
                
                // Ищем актуальный индекс по ID, а не используем сохраненный
                if let currentIndex = self.items.firstIndex(where: { $0.id == itemID }) {
                    self.items.remove(at: currentIndex)
                    self.tableView.deleteRows(at: [IndexPath(row: currentIndex, section: 0)], with: .automatic)
                }
            }
        ])
    }
}
2. Взаимодействие с UIViewPropertyAnimator — если у вас есть анимации, которые могут выполняться во время вызова контекстного меню, они могут конфликтовать:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Проблема: параллельная анимация может мешать анимации контекстного меню
let fadeAnimator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut) {
    self.menuView.alpha = 0.5
}
fadeAnimator.startAnimation()
 
// Одновременно пользователь может вызвать контекстное меню,
// что приведет к странному поведению анимации
 
// Решение: приостанавливать текущие анимации при вызове меню
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
    // Приостанавливаем все активные аниматоры
    for animator in activeAnimators {
        animator.pauseAnimation()
    }
}
 
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
    // Возобновляем все приостановленные анимации
    for animator in activeAnimators {
        animator.continueAnimation(withTimingParameters: nil, durationFactor: 1.0)
    }
}
3. Ограничения на количество вложенных меню — iOS имеет ограничение на глубину вложенности меню. Обычно это не проблема, но если вы создаете сложные иерархические меню, учитывайте это:

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
// Старайтесь избегать слишком глубокой вложенности
// Плохо: меню внутри меню внутри меню внутри меню
let subSubSubMenu = UIMenu(title: "Сильно вложенное меню", children: [/* действия */])
let subSubMenu = UIMenu(title: "Подподменю", children: [subSubSubMenu, /* другие действия */])
let subMenu = UIMenu(title: "Подменю", children: [subSubMenu, /* другие действия */])
let mainMenu = UIMenu(title: "", children: [subMenu, /* другие действия */])
 
// Лучше: ограничьте вложенность двумя-тремя уровнями
// и используйте displayInline где это уместно
let formattingActions = [
    UIAction(title: "Полужирный", image: UIImage(systemName: "bold")) { _ in },
    UIAction(title: "Курсив", image: UIImage(systemName: "italic")) { _ in }
]
 
let colorActions = [
    UIAction(title: "Красный", image: nil) { _ in },
    UIAction(title: "Синий", image: nil) { _ in }
]
 
// Группируем связанные действия с displayInline
let formattingMenu = UIMenu(title: "Форматирование", options: .displayInline, children: formattingActions)
let colorMenu = UIMenu(title: "Цвет", options: .displayInline, children: colorActions)
 
// Основное меню содержит группы, но без лишней вложенности
let mainMenu = UIMenu(title: "", children: [formattingMenu, colorMenu, /* другие действия */])
4. Проблемы с памятью при показе предпросмотра — если ваш предпросмотр содержит тяжелые ресурсы, это может привести к проблемам с памятью:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Проблема: загрузка слишком большого изображения в предпросмотре
func previewProvider() -> UIViewController {
    let vc = PreviewViewController()
    
    // Загружаем полноразмерное изображение - это может привести к проблемам с памятью
    vc.imageView.image = UIImage(named: "very_large_image")
    
    return vc
}
 
// Решение: использовать уменьшенные версии ресурсов для предпросмотра
func previewProvider() -> UIViewController {
    let vc = PreviewViewController()
    
    // Используем миниатюру вместо полного изображения
    vc.imageView.image = UIImage(named: "thumbnail_image")
    
    // Загружаем полное изображение только если пользователь решит открыть детальное представление
    return vc
}
5. Проблемы с отзывчивостью при использовании Haptic Touch — на устройствах без 3D Touch (iPhone XR и новее) контекстные меню вызываются через Haptic Touch, который требует более длительного нажатия:

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
// Можно попробовать уменьшить время удержания для Haptic Touch
// Обратите внимание: это использует приватное API и не рекомендуется для App Store
if let interactions = view.interactions.compactMap({ $0 as? UIContextMenuInteraction }),
   let interaction = interactions.first,
   let gestureRecognizer = interaction.value(forKey: "_gestureRecognizer") as? UILongPressGestureRecognizer {
    
    // Уменьшаем время нажатия
    gestureRecognizer.minimumPressDuration = 0.3 // По умолчанию это примерно 0.5 секунд
}
 
// Более правильный подход - предложить пользователям альтернативный способ доступа к тем же функциям
override func viewDidLoad() {
    super.viewDidLoad()
    
    // Добавьте явную кнопку или свайп как альтернативный способ доступа к функциям контекстного меню
    let moreButton = UIButton(type: .system)
    moreButton.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)
    moreButton.addTarget(self, action: #selector(showOptionsMenu), for: .touchUpInside)
    view.addSubview(moreButton)
    
    // Настройте положение кнопки...
}
 
@objc func showOptionsMenu(_ sender: UIButton) {
    // Показываем те же опции, но через альтернативный интерфейс
    let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
    
    // Добавляем те же действия, что и в контекстном меню
    actionSheet.addAction(UIAlertAction(title: "Просмотреть", style: .default) { _ in })
    actionSheet.addAction(UIAlertAction(title: "Поделиться", style: .default) { _ in })
    actionSheet.addAction(UIAlertAction(title: "Удалить", style: .destructive) { _ in })
    actionSheet.addAction(UIAlertAction(title: "Отмена", style: .cancel))
    
    // На iPad нужно указать источник для всплывающего меню
    if let popover = actionSheet.popoverPresentationController {
        popover.sourceView = sender
        popover.sourceRect = sender.bounds
    }
    
    present(actionSheet, animated: true)
}

Доступность и контекстные меню



Очень важный аспект, который часто упускают из виду — доступность контекстных меню для пользователей с ограниченными возможностями:

1. VoiceOver поддержка — убедитесь, что ваши меню правильно озвучиваются VoiceOver:

Swift
1
2
3
4
5
6
7
8
9
10
11
// Добавляем подсказки для VoiceOver
menuView.isAccessibilityElement = true
menuView.accessibilityLabel = "Изображение горы"
menuView.accessibilityHint = "Долгое нажатие для просмотра опций"
 
// Можно также добавить кастомные действия доступности
menuView.accessibilityCustomActions = [
    UIAccessibilityCustomAction(name: "Просмотреть", target: self, selector: #selector(viewImage)),
    UIAccessibilityCustomAction(name: "Поделиться", target: self, selector: #selector(shareImage)),
    UIAccessibilityCustomAction(name: "Удалить", target: self, selector: #selector(deleteImage))
]
2. Альтернативы для пользователей с моторными нарушениями — долгое нажатие может быть сложным для некоторых пользователей:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Добавляем явную кнопку для доступа к тем же функциям
func setupAccessibleAlternative() {
    let accessButton = UIButton(type: .system)
    accessButton.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)
    accessButton.addTarget(self, action: #selector(showAccessibleMenu), for: .touchUpInside)
    
    // Размещаем кнопку рядом с основным элементом
    view.addSubview(accessButton)
    
    // Настраиваем констрейнты...
}
 
@objc func showAccessibleMenu() {
    // Показываем альтернативное меню, доступное через одно касание
}
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
// Адаптируем размер предпросмотра к размеру шрифта
func adaptPreviewSizeForAccessibility() -> CGSize {
    let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory
    
    // Если выбран увеличенный размер, делаем предпросмотр больше
    if contentSizeCategory >= .accessibilityMedium {
        return CGSize(width: 300, height: 400)
    } else {
        return CGSize(width: 250, height: 350)
    }
}
 
// Адаптируем меню для пользователей с увеличенным текстом
func createAccessibilityFriendlyMenu() -> UIMenu {
    let isLargeText = UIApplication.shared.preferredContentSizeCategory >= .accessibilityMedium
    
    var actions: [UIMenuElement] = []
    
    // Для пользователей с очень крупным текстом упрощаем меню
    if isLargeText {
        // Оставляем только самые важные действия
        actions.append(UIAction(title: "Просмотреть", image: UIImage(systemName: "eye")) { _ in })
        actions.append(UIAction(title: "Поделиться", image: UIImage(systemName: "square.and.arrow.up")) { _ in })
        actions.append(UIAction(title: "Удалить", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in })
    } else {
        // Используем полный набор действий для обычных пользователей
        // ...
    }
    
    return UIMenu(title: "", children: actions)
}

Будущее контекстных меню в iOS



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

1. Используйте стандартные компоненты и API — Apple часто оптимизирует и улучшает существующие API, поэтому стандартные реализации получают улучшения "бесплатно":

Swift
1
2
3
4
5
6
7
// Предпочитайте использовать стандартные API вместо создания собственных решений
// Плохо: создание собственной системы меню
view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(handleCustomLongPress)))
 
// Хорошо: использование стандартного API
let interaction = UIContextMenuInteraction(delegate: self)
view.addInteraction(interaction)
2. Адаптируйте вашу кодовую базу к изменениям API — создавайте обертки вокруг API контекстных меню, чтобы легко адаптироваться к изменениям:

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
// Создаем обертку вокруг функциональности контекстного меню
class ContextMenuManager {
    static let shared = ContextMenuManager()
    
    func createContextMenu(for model: Any, with actions: [MenuAction]) -> UIContextMenuConfiguration {
        return UIContextMenuConfiguration(identifier: nil, previewProvider: {
            self.createPreviewFor(model)
        }) { _ in
            self.createMenuFor(actions: actions)
        }
    }
    
    private func createPreviewFor(_ model: Any) -> UIViewController? {
        // Логика создания предпросмотра
        return nil
    }
    
    private func createMenuFor(actions: [MenuAction]) -> UIMenu {
        let uiActions = actions.map { action in
            return UIAction(
                title: action.title,
                image: action.image,
                attributes: action.isDestructive ? .destructive : [],
                handler: { _ in action.handler() }
            )
        }
        
        return UIMenu(title: "", children: uiActions)
    }
}
 
// Использование:
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    let item = items[indexPath.row]
    
    let actions = [
        MenuAction(title: "Просмотр", image: UIImage(systemName: "eye"), handler: { [weak self] in
            self?.viewItem(item)
        }),
        MenuAction(title: "Удалить", image: UIImage(systemName: "trash"), isDestructive: true, handler: { [weak self] in
            self?.deleteItem(item)
        })
    ]
    
    return ContextMenuManager.shared.createContextMenu(for: item, with: actions)
}
 
// Если API изменится в будущем, вам нужно будет изменить только ContextMenuManager

FaceId свои иконки для контекстных меню
Здравствуйте! Подскажите решение для своих иконок контекстных меню. Public Sub RightClickEmptySpasce() Dim cmdBAR As CommandBar ...

Как передать информацию в вызываемую форму из контекстных меню различных объектов главной формы
Здравствуйте. Мне необходимо решить следующую задачу. Есть форма с рисунками (см. Рисунок 1). При нажатии правой кнопкой мыши на каждый рисунок...

Набор команды. Разработка/продвижение сайта, разработка приложения на андройд и ios
Есть идея, ищу энтузиастов готовые работать за идею. Думаю дело выгорит, только нужно быть первым. Все подробности в лс. Плата в процентах от доходов...

Разработка под iOS
Привет С чего начать разработку под iOS? Имею ввиду, что для этого нужно знать, что учить? Какие языки надо знать? Какие инструменты для...

Разработка под iOS на C#
Доброго времени суток, уважаемые коллеги!!! :drink: Загорелся идеей разработки приложения под iOS для всяких моднявых гаджетов типо айвонь,...

IOS разработка без Mac
Привет ребята! Сразу прошу почитателей Apple не кидаться помидорками. У меня нет iPhone, у меня нет MacBook. Но я хотел бы попробовать...

Разработка приложения на Android и iOS
1. Это примерный UI дизайн для фитнесс апп с функциями 2. Покупки в приложении (in app purvchase) или месячный абонемент (monthly subscription) ...

IOS разработка без Mac
Привет ребята! Сразу прошу почитателей Apple не кидаться помидорками. У меня нет iPhone, у меня нет MacBook. Но я хотел бы попробовать...

Разработка на Java под iOS
На днях вспомнилось, что java великолепна своей мультиплатформенностью. Интересно, а возможно ли писать на java под iOS. Есть ли литература по...

Разработка под IOS в Unity
Добрый день, уважаемые форумчане. Недавно закончил свой проект для IOS в Unity и скомпилировал его. После всего этого, я загрузил своё приложение в...

Разработка под iOS Delphi XE5
Ребят, нужен ли сертификат программе разработанной в делфи ? Как его обойти? Можно ли после компиляции сразу закачать на iPhone и использовать прогу ?

Разработка под Android/iOS в продакшене
Всем привет! Уважаемые профи, подскажите. Готов ли Qt 5.7 для разработки под мобильные платформы? В случае положительного ответа. После подключения...

Метки ios, mobile, swift, swiftui
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Unity 4D
GameUnited 13.06.2025
Четырехмерное пространство. . . Звучит как что-то из научной фантастики, правда? Однако для меня, как разработчика со стажем в игровой индустрии, четвертое измерение давно перестало быть абстракцией из. . .
SSE (Server-Sent Events) в ASP.NET Core и .NET 10
UnmanagedCoder 13.06.2025
Кажется, Microsoft снова подкинула нам интересную фичу в новой версии фреймворка. Работая с превью . NET 10, я наткнулся на нативную поддержку Server-Sent Events (SSE) в ASP. NET Core Minimal APIs. Эта. . .
С днём независимости России!
Hrethgir 13.06.2025
Решил побеседовать, с утра праздничного дня, с LM о завоеваниях. То что она написала о народе, представителем которого я являюсь сам сначала возмутило меня, но дальше только смешило. Это чисто. . .
Лето вокруг.
kumehtar 13.06.2025
Лето вокруг. Наполненное бурями и ураганами событий. На фоне магии Жизни, священной и вечной, неумелой рукой человека рисуется панорама душевного непокоя. Странные серые краски проникают и. . .
Популярные LM модели ориентированы на увеличение затрат ресурсов пользователями сгенерированного кода (грязь -заслуги чистоплюев).
Hrethgir 12.06.2025
Вообще обратил внимание, что они генерируют код (впрочем так-же ориентированы разработчики чипов даже), чтобы пользователь их использующий уходил в тот или иной убыток. Это достаточно опытные модели,. . .
Топ10 библиотек C для квантовых вычислений
bytestream 12.06.2025
Квантовые вычисления - это та область, где теория встречается с практикой на границе наших знаний о физике. Пока большая часть шума вокруг квантовых компьютеров крутится вокруг языков высокого уровня. . .
Dispose и Finalize в C#
stackOverflow 12.06.2025
Работая с C# больше десяти лет, я снова и снова наблюдаю одну и ту же историю: разработчики наивно полагаются на сборщик мусора, как на волшебную палочку, которая решит все проблемы с памятью. Да,. . .
Повышаем производительность игры на Unity 6 с GPU Resident Drawer
GameUnited 11.06.2025
Недавно копался в новых фичах Unity 6 и наткнулся на GPU Resident Drawer - штуку, которая заставила меня присвистнуть от удивления. По сути, это внутренний механизм рендеринга, который автоматически. . .
Множества в Python
py-thonny 11.06.2025
В Python существует множество структур данных, но иногда я сталкиваюсь с задачами, где ни списки, ни словари не дают оптимального решения. Часто это происходит, когда мне нужно быстро проверять. . .
Работа с ccache/sccache в рамках C++
Loafer 11.06.2025
Утилиты ccache и sccache занимаются тем, что кешируют промежуточные результаты компиляции, таким образом ускоряя последующие компиляции проекта. Это означает, что если проект будет компилироваться. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru
OSZAR »