Блог Каргина
Про технологии и жизнь

С помощью Activity View Controller можно предоставить пользователю функции для взаимодействия с контентом в других приложениях: копирования, публикации в социальные сети, отправки через мессенджеры и многие другие функции.

К сожалению, Apple до сих пор не представили нормального способа создания UIActivityViewController в контексте SwiftUI. Как и не сделали ещё больше десятка мелких, но необходимых вещей для базового приложения.

Самым простым и логичным решением кажется оборачивание в UIViewControllerRepresentable, как в топовом ответе на StackOverflow, и представление через .sheet().

struct ActivityViewController: UIViewControllerRepresentable { var activityItems: [Any] var applicationActivities: [UIActivity]? = nil func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController { return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) } func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {} }

Всё это хорошо работает на iPhone. Однако у этой реализации есть один большой недостаток: Activity View будет занимать весь экран iPad, а в macOS меню будет торчать не там, где нужно.

Представление Activity View курильщика в iPadOS и macOS.

Основная причина некорректного поведения в том, что в UIKit приложении для представления UIActivityViewController требуется указывать sourceView и sourceRect, что явно указано в документации. Приложение на iPad или macOS просто упало бы без этих атрибутов, но в SwiftUI поведение изменили.

On iPad, you must present the view controller in a popover. On iPhone and iPod touch, you must present it modally.

Представление Activity View здорового человека в iPadOS и macOS.

Если вы используете SwiftUI из UIKit, то просто передайте координаты и размеры вашей кнопки в UIViewController. Имея координаты, можно установить необходимую позицию для UIActivityViewController. Получить координаты для View можно через .overlay() и GeometryReader. Не очень красиво, но работает.

protocol MyUIViewDelegate: AnyObject { func shareButtonFrameChanged(frame: CGRect) } .overlay(GeometryReader { proxy -> AnyView in delegate?.shareButtonFrameChanged( frame: proxy.frame(in: CoordinateSpace.global) ) return AnyView(Rectangle().fill(.clear)) })

Что делать, если вы хотите писать только на SwiftUI? Я попытался воссоздать нормальное поведение через popover. В iPadOS меню начало отображаться ровно по середине кнопки, но откорректировать его положение не получилось. В macOS поведение вообще не изменилось.

Но приложение нужно сделать уже позавчера, поэтому будем хакать поведение. Apple уже несколько лет не может разобраться с поддержкой нескольких окон в iPadOS, поэтому нужно добавить несколько магических расширений для того, чтобы получить UIViewController, в котором размещается кнопка шаринга.

extension UIApplication { var keyWindow: UIWindow? { return UIApplication.shared.connectedScenes .filter { $0.activationState == .foregroundActive } .first(where: { $0 is UIWindowScene }) .flatMap({ $0 as? UIWindowScene })?.windows .first(where: \.isKeyWindow) } var keyWindowPresentedController: UIViewController? { var viewController = self.keyWindow?.rootViewController if let presentedController = viewController as? UITabBarController { viewController = presentedController.selectedViewController } while let presentedController = viewController?.presentedViewController { if let presentedController = presentedController as? UITabBarController { viewController = presentedController.selectedViewController } else { viewController = presentedController } } return viewController } }

Кнопку шаринга нужно разместить в Geometry Reader, чтобы получить её координаты. .overlay() уже не подойдёт, так как он будет изменять состояние View, но и код станет проще.

GeometryReader { proxy in Button(action: { let rect = proxy.frame(in: CoordinateSpace.global) let activityViewController = UIActivityViewController(activityItems: [URL(string: "https://karh.in/")!], applicationActivities: nil) if let vc = UIApplication.shared.keyWindowPresentedController { activityViewController.popoverPresentationController?.sourceView = vc.view activityViewController.popoverPresentationController?.sourceRect = rect vc.present(activityViewController, animated: true, completion: nil) } }, label: { Image(systemName: "square.and.arrow.up") .font(.system(size: 32)) }).buttonStyle(.bordered) }.frame(width: 48, height: 48)

Плохая новость: это может перестать работать в следующих версиях 🤡. Хорошая новость в том, что теперь Activity View будет появляться в нужном месте, а если даже поведение окон поменяют, то приложение не упадёт, а просто не отобразит меню.

Представление Activity View в SwiftUI здорового человека.

Полезные ссылки по теме:

Если вдруг есть какой-то более красивый способ правильно отобразить UIActivityViewController на всех платформах, то обязательно поделитесь в комментариях. Надеюсь, что на WWDC 2022 всё таки представят нормально решение.

Диджитал iOS iPadOS Swift SwiftUI Программирование UIKit

Чтобы оставить комметарий, войдите в свой аккаунт.

Вход

Первый iPhone
Первый iPhone

Возле первого айфона часто вижу приписки «Революционный», «Прорывной» и так далее. У меня совсем другое мнение по этому поводу.

721
DALL-E: я снова верю в технологии
DALL-E: я снова верю в технологии

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

171
Криптография на пальцах
Криптография на пальцах

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

131
Красивый Email за десять минут
Красивый Email за десять минут

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

710