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