Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 233 lines 6.6 kB view raw
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 // Native content height observation (eliminates JS bridge round-trip) 12 private var contentHeightObservation: NSKeyValueObservation? 13 14 // Events 15 private let onAttemptDismiss = EventDispatcher() 16 private let onSnapPointChange = EventDispatcher() 17 private let onStateChange = EventDispatcher() 18 19 // Open event firing 20 private var isOpen: Bool = false { 21 didSet { 22 onStateChange([ 23 "state": isOpen ? "open" : "closed" 24 ]) 25 } 26 } 27 28 // React view props 29 var fullHeight = false 30 var preventDismiss = false 31 var preventExpansion = false 32 var cornerRadius: CGFloat? 33 var sourceViewTag: Int? 34 var minHeight = 0.0 35 var maxHeight: CGFloat! { 36 didSet { 37 let screenHeight = Util.getScreenHeight() ?? 0 38 if maxHeight > screenHeight { 39 maxHeight = screenHeight 40 } 41 } 42 } 43 44 private var isOpening = false { 45 didSet { 46 if isOpening { 47 onStateChange([ 48 "state": "opening" 49 ]) 50 } 51 } 52 } 53 private var isClosing = false { 54 didSet { 55 if isClosing { 56 onStateChange([ 57 "state": "closing" 58 ]) 59 } 60 } 61 } 62 private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? { 63 didSet { 64 if selectedDetentIdentifier == .large { 65 onSnapPointChange([ 66 "snapPoint": 2 67 ]) 68 } else { 69 onSnapPointChange([ 70 "snapPoint": 1 71 ]) 72 } 73 } 74 } 75 76 // MARK: - Lifecycle 77 78 required init (appContext: AppContext? = nil) { 79 super.init(appContext: appContext) 80 self.maxHeight = Util.getScreenHeight() 81 self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge) 82 SheetManager.shared.add(self) 83 } 84 85 deinit { 86 self.destroy() 87 } 88 89 // We don't want this view to actually get added to the tree, so we'll simply store it for adding 90 // to the SheetViewController 91 override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { 92 self.touchHandler?.attach(to: subview) 93 self.innerView = subview 94 } 95 96 // We'll grab the content height from here so we know the initial detent to set 97 override func layoutSubviews() { 98 super.layoutSubviews() 99 100 guard let innerView = self.innerView else { 101 return 102 } 103 104 if innerView.subviews.count != 1 { 105 return 106 } 107 108 self.present() 109 } 110 111 private func destroy() { 112 self.contentHeightObservation?.invalidate() 113 self.contentHeightObservation = nil 114 self.isClosing = false 115 self.isOpen = false 116 self.sheetVc = nil 117 self.touchHandler?.detach(from: self.innerView) 118 self.touchHandler = nil 119 self.innerView = nil 120 SheetManager.shared.remove(self) 121 } 122 123 // MARK: - Presentation 124 125 func present() { 126 guard !self.isOpen, 127 !self.isOpening, 128 !self.isClosing, 129 let innerView = self.innerView, 130 let contentHeight = innerView.subviews.first?.frame.height, 131 let rvc = self.reactViewController() else { 132 return 133 } 134 135 let sheetVc = SheetViewController() 136 sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion, fullHeight: self.fullHeight) 137 if let sheet = sheetVc.sheetPresentationController { 138 sheet.delegate = self 139 sheet.preferredCornerRadius = self.cornerRadius 140 self.selectedDetentIdentifier = sheet.selectedDetentIdentifier 141 } 142 sheetVc.view.addSubview(innerView) 143 144 if #available(iOS 26.0, *), 145 let tag = self.sourceViewTag, 146 let bridge = self.appContext?.reactBridge, 147 let sourceView = bridge.uiManager.view(forReactTag: NSNumber(value: tag)) { 148 sheetVc.preferredTransition = .zoom { _ in 149 return sourceView 150 } 151 } 152 153 self.sheetVc = sheetVc 154 self.isOpening = true 155 if !self.fullHeight { 156 self.startObservingContentHeight() 157 } 158 159 rvc.present(sheetVc, animated: true) { [weak self] in 160 self?.isOpening = false 161 self?.isOpen = true 162 } 163 } 164 165 // Observe the content view's bounds via KVO so that height changes are detected 166 // purely on the native side, without a JS bridge round-trip through onLayout. 167 // Calls updateDetents directly with the observed height rather than going through 168 // updateLayout(), which has a prevLayoutDetentIdentifier guard that can block 169 // legitimate content-driven updates when detent identifiers drift during animations. 170 private func startObservingContentHeight() { 171 self.contentHeightObservation?.invalidate() 172 173 guard let contentView = self.innerView?.subviews.first else { return } 174 175 self.contentHeightObservation = contentView.observe( 176 \.bounds, 177 options: [.old, .new] 178 ) { [weak self] _, change in 179 guard let self = self, 180 (self.isOpen || self.isOpening) && !self.isClosing, 181 let oldBounds = change.oldValue, 182 let newBounds = change.newValue, 183 oldBounds.height != newBounds.height, 184 newBounds.height > 0 else { return } 185 let clampedHeight = self.clampHeight(newBounds.height) 186 self.sheetVc?.updateDetents(contentHeight: clampedHeight, preventExpansion: self.preventExpansion) 187 self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier() 188 } 189 } 190 191 func dismiss() { 192 guard let sheetVc = self.sheetVc else { 193 return 194 } 195 196 self.isClosing = true 197 DispatchQueue.main.async { 198 sheetVc.dismiss(animated: true) { [weak self] in 199 self?.destroy() 200 } 201 } 202 } 203 204 // MARK: - Utils 205 206 private func clampHeight(_ height: CGFloat) -> CGFloat { 207 if height < self.minHeight { 208 return self.minHeight 209 } else if height > self.maxHeight { 210 return self.maxHeight 211 } 212 return height 213 } 214 215 // MARK: - UISheetPresentationControllerDelegate 216 217 func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { 218 self.onAttemptDismiss() 219 return !self.preventDismiss 220 } 221 222 func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { 223 self.isClosing = true 224 } 225 226 func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { 227 self.destroy() 228 } 229 230 func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { 231 self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier 232 } 233}