| 1 | import ExpoModulesCore |
| 2 | import UIKit |
| 3 | |
| 4 | final class T3PresentationSources { |
| 5 | private class Entry { |
| 6 | weak var view: UIView? |
| 7 | init(_ view: UIView) { self.view = view } |
| 8 | } |
| 9 | |
| 10 | private var entries: [String: Entry] = [:] |
| 11 | |
| 12 | func register(_ view: UIView, identifier: String) { |
| 13 | entries[identifier] = Entry(view) |
| 14 | } |
| 15 | |
| 16 | func remove(_ view: UIView, identifier: String) { |
| 17 | if entries[identifier]?.view == nil || entries[identifier]?.view === view { |
| 18 | entries.removeValue(forKey: identifier) |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | func view(for identifier: String) -> UIView? { |
| 23 | // Use the child bounds, not the wrapper's potentially stretched layout bounds. |
| 24 | entries[identifier]?.view?.subviews.first |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | final class T3PresentationSourceView: ExpoView { |
| 29 | weak var sources: T3PresentationSources? |
| 30 | var identifier = "" { |
| 31 | didSet { |
| 32 | sources?.remove(self, identifier: oldValue) |
| 33 | if !identifier.isEmpty { sources?.register(self, identifier: identifier) } |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | deinit { |
| 38 | sources?.remove(self, identifier: identifier) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | /// Sizes the containing React Native modal without replacing UIKit's sheet interaction. |
| 43 | final class T3ContextSheetSizeView: ExpoView { |
| 44 | var contentHeight: CGFloat = 0 { |
| 45 | didSet { updateSheet() } |
| 46 | } |
| 47 | private weak var configuredSheet: UISheetPresentationController? |
| 48 | private var appliedHeight: CGFloat = 0 |
| 49 | |
| 50 | override func didMoveToWindow() { |
| 51 | super.didMoveToWindow() |
| 52 | // The modal's presentation controller is attached after the content view. |
| 53 | DispatchQueue.main.async { [weak self] in self?.updateSheet() } |
| 54 | } |
| 55 | |
| 56 | override func layoutSubviews() { |
| 57 | super.layoutSubviews() |
| 58 | updateSheet() |
| 59 | } |
| 60 | |
| 61 | private func updateSheet() { |
| 62 | guard window != nil, contentHeight > 0 else { return } |
| 63 | var responder: UIResponder? = self |
| 64 | while let current = responder { |
| 65 | if let controller = current as? UIViewController, |
| 66 | controller.presentingViewController != nil, |
| 67 | let sheet = controller.sheetPresentationController { |
| 68 | guard configuredSheet !== sheet || abs(appliedHeight - contentHeight) > 1 else { return } |
| 69 | configuredSheet = sheet |
| 70 | appliedHeight = contentHeight |
| 71 | let height = contentHeight |
| 72 | let identifier = UISheetPresentationController.Detent.Identifier("t3-context-content") |
| 73 | sheet.animateChanges { |
| 74 | sheet.detents = [.custom(identifier: identifier) { context in |
| 75 | min(height, context.maximumDetentValue * 0.92) |
| 76 | }] |
| 77 | sheet.selectedDetentIdentifier = identifier |
| 78 | sheet.prefersGrabberVisible = true |
| 79 | sheet.prefersScrollingExpandsWhenScrolledToEdge = false |
| 80 | } |
| 81 | return |
| 82 | } |
| 83 | responder = current.next |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func presentFileShare( |
| 89 | url: URL, |
| 90 | title: String, |
| 91 | source: UIView?, |
| 92 | presenter: UIViewController, |
| 93 | promise: Promise |
| 94 | ) throws { |
| 95 | guard url.isFileURL, FileManager.default.isReadableFile(atPath: url.path) else { |
| 96 | throw NSError( |
| 97 | domain: "T3NativePresentation", |
| 98 | code: 1, |
| 99 | userInfo: [NSLocalizedDescriptionKey: "The file is no longer available."] |
| 100 | ) |
| 101 | } |
| 102 | |
| 103 | guard let origin = source ?? presenter.view else { |
| 104 | throw NSError( |
| 105 | domain: "T3NativePresentation", |
| 106 | code: 2, |
| 107 | userInfo: [NSLocalizedDescriptionKey: "The presenting screen is no longer open."] |
| 108 | ) |
| 109 | } |
| 110 | |
| 111 | let activity = UIActivityViewController(activityItems: [url], applicationActivities: nil) |
| 112 | activity.title = title |
| 113 | activity.overrideUserInterfaceStyle = source?.traitCollection.userInterfaceStyle |
| 114 | ?? presenter.traitCollection.userInterfaceStyle |
| 115 | activity.completionWithItemsHandler = { _, _, _, _ in promise.resolve(nil) } |
| 116 | if presenter.traitCollection.userInterfaceIdiom == .pad { |
| 117 | activity.popoverPresentationController?.sourceView = origin |
| 118 | activity.popoverPresentationController?.sourceRect = source?.bounds |
| 119 | ?? CGRect(x: origin.bounds.midX, y: origin.bounds.midY, width: 1, height: 1) |
| 120 | } else { |
| 121 | // Let UIKit adapt the remote share scene to the phone, not an anchored popover. |
| 122 | activity.modalPresentationStyle = .automatic |
| 123 | } |
| 124 | presenter.present(activity, animated: true) |
| 125 | } |