Karhin’s Blog
About technologies and life

С помощью 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 всё таки представят нормально решение.

Digital iOS iPadOS Swift SwiftUI Программирование UIKit

To post a comment, please log in or create an account.

Sign In

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

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

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

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

818
Как слушать YouTube видео в фоне и смотреть картинку-в-картинке без подписки на iOS
Как слушать YouTube видео в фоне и смотреть картинку-в-картинке без подписки на iOS

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

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

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

847