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

ДиджиталiOSiPadOSmacOSSwiftSwiftUIПрограммирование
Блог Каргина

Подпишитесь на блог

Чтобы не пропускать новые записи и поддержать работу

kgnk

В сториз анонсы новых записей и проектов. В ленте фотографии, которые я делаю.

Instagram
nikolaykargin

В основным ссылки на новые посты, лайки котиков и несмешные шутки.

Twitter
mikalaj_karhin

Анонсы новых записей, ссылки и заметки, которые не подходят под формат блога.

Telegram

Комментарии

Почитайте ещё это

Стив Джобс про файловую систему в 2005 году
Стив Джобс про файловую систему в 2005 году

Стив Джобс про файловую систему в 2005 году

Фотографии должны быть в приложении «Фото», а песни в приложении «Музыка», потому что они лучше умеют работать с этими файлами. Finder будет нужен только тем, кто с ним умеет работать.

Git за пять минут
Git за пять минут

Git за пять минут

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