Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 210 lines 5.8 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 // 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}