forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}