Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import ExpoModulesCore
2import React
3import UIKit
4
5class SheetView: ExpoView, UISheetPresentationControllerDelegate {
6 // Views
7 private var sheetVc: SheetViewController?
8 private var innerView: UIView?
9 private var touchHandler: RCTTouchHandler?
10
11 // Events
12 private let onAttemptDismiss = EventDispatcher()
13 private let onSnapPointChange = EventDispatcher()
14 private let onStateChange = EventDispatcher()
15
16 // Open event firing
17 private var isOpen: Bool = false {
18 didSet {
19 onStateChange([
20 "state": isOpen ? "open" : "closed"
21 ])
22 }
23 }
24
25 // React view props
26 var preventDismiss = false
27 var preventExpansion = false
28 var cornerRadius: CGFloat?
29 var sourceViewTag: Int?
30 var minHeight = 0.0
31 var maxHeight: CGFloat! {
32 didSet {
33 let screenHeight = Util.getScreenHeight() ?? 0
34 if maxHeight > screenHeight {
35 maxHeight = screenHeight
36 }
37 }
38 }
39
40 private var isOpening = false {
41 didSet {
42 if isOpening {
43 onStateChange([
44 "state": "opening"
45 ])
46 }
47 }
48 }
49 private var isClosing = false {
50 didSet {
51 if isClosing {
52 onStateChange([
53 "state": "closing"
54 ])
55 }
56 }
57 }
58 private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? {
59 didSet {
60 if selectedDetentIdentifier == .large {
61 onSnapPointChange([
62 "snapPoint": 2
63 ])
64 } else {
65 onSnapPointChange([
66 "snapPoint": 1
67 ])
68 }
69 }
70 }
71 private var prevLayoutDetentIdentifier: UISheetPresentationController.Detent.Identifier?
72
73 // MARK: - Lifecycle
74
75 required init (appContext: AppContext? = nil) {
76 super.init(appContext: appContext)
77 self.maxHeight = Util.getScreenHeight()
78 self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge)
79 SheetManager.shared.add(self)
80 }
81
82 deinit {
83 self.destroy()
84 }
85
86 // We don't want this view to actually get added to the tree, so we'll simply store it for adding
87 // to the SheetViewController
88 override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
89 self.touchHandler?.attach(to: subview)
90 self.innerView = subview
91 }
92
93 // We'll grab the content height from here so we know the initial detent to set
94 override func layoutSubviews() {
95 super.layoutSubviews()
96
97 guard let innerView = self.innerView else {
98 return
99 }
100
101 if innerView.subviews.count != 1 {
102 return
103 }
104
105 self.present()
106 }
107
108 private func destroy() {
109 self.isClosing = false
110 self.isOpen = false
111 self.sheetVc = nil
112 self.touchHandler?.detach(from: self.innerView)
113 self.touchHandler = nil
114 self.innerView = nil
115 SheetManager.shared.remove(self)
116 }
117
118 // MARK: - Presentation
119
120 func present() {
121 guard !self.isOpen,
122 !self.isOpening,
123 !self.isClosing,
124 let innerView = self.innerView,
125 let contentHeight = innerView.subviews.first?.frame.height,
126 let rvc = self.reactViewController() else {
127 return
128 }
129
130 let sheetVc = SheetViewController()
131 sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
132 if let sheet = sheetVc.sheetPresentationController {
133 sheet.delegate = self
134 sheet.preferredCornerRadius = self.cornerRadius
135 self.selectedDetentIdentifier = sheet.selectedDetentIdentifier
136 }
137 sheetVc.view.addSubview(innerView)
138
139 if #available(iOS 26.0, *),
140 let tag = self.sourceViewTag,
141 let bridge = self.appContext?.reactBridge,
142 let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: tag)) {
143 sheetVc.preferredTransition = .zoom { _ in
144 return sourceView
145 }
146 }
147
148 self.sheetVc = sheetVc
149 self.isOpening = true
150
151 rvc.present(sheetVc, animated: true) { [weak self] in
152 self?.isOpening = false
153 self?.isOpen = true
154 }
155 }
156
157 func updateLayout() {
158 // Allow updates either when identifiers match OR when prevLayoutDetentIdentifier is nil (first real content update)
159 if self.prevLayoutDetentIdentifier == self.selectedDetentIdentifier || self.prevLayoutDetentIdentifier == nil,
160 let contentHeight = self.innerView?.subviews.first?.frame.size.height {
161 self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
162 preventExpansion: self.preventExpansion)
163 self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier()
164 }
165 self.prevLayoutDetentIdentifier = self.selectedDetentIdentifier
166 }
167
168 func dismiss() {
169 guard let sheetVc = self.sheetVc else {
170 return
171 }
172
173 self.isClosing = true
174 DispatchQueue.main.async {
175 sheetVc.dismiss(animated: true) { [weak self] in
176 self?.destroy()
177 }
178 }
179 }
180
181 // MARK: - Utils
182
183 private func clampHeight(_ height: CGFloat) -> CGFloat {
184 if height < self.minHeight {
185 return self.minHeight
186 } else if height > self.maxHeight {
187 return self.maxHeight
188 }
189 return height
190 }
191
192 // MARK: - UISheetPresentationControllerDelegate
193
194 func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
195 self.onAttemptDismiss()
196 return !self.preventDismiss
197 }
198
199 func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
200 self.isClosing = true
201 }
202
203 func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
204 self.destroy()
205 }
206
207 func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
208 self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier
209 }
210}