1import ExpoModulesCore
2import UIKit
3
4final 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
28final 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
42final class T3ContextSheetSizeView: ExpoView {
43 var contentHeight: CGFloat = 0 {
44 didSet { updateSheet() }
45 }
46 private weak var configuredSheet: UISheetPresentationController?
47 private var appliedHeight: CGFloat = 0
48
49 override func didMoveToWindow() {
50 super.didMoveToWindow()
51 // The modal's presentation controller is attached after the content view.
52 DispatchQueue.main.async { [weak self] in self?.updateSheet() }
53 }
54
55 override func layoutSubviews() {
56 super.layoutSubviews()
57 updateSheet()
58 }
59
60 private func updateSheet() {
61 guard window != nil, contentHeight > 0 else { return }
62 var responder: UIResponder? = self
63 while let current = responder {
64 if let controller = current as? UIViewController,
65 controller.presentingViewController != nil,
66 let sheet = controller.sheetPresentationController {
67 guard configuredSheet !== sheet || abs(appliedHeight - contentHeight) > 1 else { return }
68 configuredSheet = sheet
69 appliedHeight = contentHeight
70 let height = contentHeight
71 let identifier = UISheetPresentationController.Detent.Identifier("t3-context-content")
72 sheet.animateChanges {
73 sheet.detents = [.custom(identifier: identifier) { context in
74 min(height, context.maximumDetentValue * 0.92)
75 }]
76 sheet.selectedDetentIdentifier = identifier
77 sheet.prefersGrabberVisible = true
78 sheet.prefersScrollingExpandsWhenScrolledToEdge = false
79 }
80 return
81 }
82 responder = current.next
83 }
84 }
85}
86
87func presentFileShare(
88 url: URL,
89 title: String,
90 source: UIView?,
91 presenter: UIViewController,
92 promise: Promise
93) throws {
94 guard url.isFileURL, FileManager.default.isReadableFile(atPath: url.path) else {
95 throw NSError(
96 domain: "T3NativePresentation",
97 code: 1,
98 userInfo: [NSLocalizedDescriptionKey: "The file is no longer available."]
99 )
100 }
101
102 guard let origin = source ?? presenter.view else {
103 throw NSError(
104 domain: "T3NativePresentation",
105 code: 2,
106 userInfo: [NSLocalizedDescriptionKey: "The presenting screen is no longer open."]
107 )
108 }
109
110 let activity = UIActivityViewController(activityItems: [url], applicationActivities: nil)
111 activity.title = title
112 activity.overrideUserInterfaceStyle = source?.traitCollection.userInterfaceStyle
113 ?? presenter.traitCollection.userInterfaceStyle
114 activity.completionWithItemsHandler = { _, _, _, _ in promise.resolve(nil) }
115 if presenter.traitCollection.userInterfaceIdiom == .pad {
116 activity.popoverPresentationController?.sourceView = origin
117 activity.popoverPresentationController?.sourceRect = source?.bounds
118 ?? CGRect(x: origin.bounds.midX, y: origin.bounds.midY, width: 1, height: 1)
119 } else {
120 // Let UIKit adapt the remote share scene to the phone, not an anchored popover.
121 activity.modalPresentationStyle = .automatic
122 }
123 presenter.present(activity, animated: true)
124}