diff options
author | Hailey <me@haileyok.com> | 2024-10-04 13:24:12 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-04 13:24:12 -0700 |
commit | 00486e94991f344353ffb083dd631283a84c3ad3 (patch) | |
tree | a5dc4da5e5e71912d73a099e84761517fa8c62a9 /modules/bottom-sheet/ios | |
parent | 9802ebe20d32dc1867a069dc377b3d4c43ce45f0 (diff) | |
download | voidsky-00486e94991f344353ffb083dd631283a84c3ad3.tar.zst |
[Sheets] [Pt. 1] Root PR (#5557)
Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: dan <dan.abramov@gmail.com> Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'modules/bottom-sheet/ios')
-rw-r--r-- | modules/bottom-sheet/ios/BottomSheet.podspec | 21 | ||||
-rw-r--r-- | modules/bottom-sheet/ios/BottomSheetModule.swift | 47 | ||||
-rw-r--r-- | modules/bottom-sheet/ios/SheetManager.swift | 28 | ||||
-rw-r--r-- | modules/bottom-sheet/ios/SheetView.swift | 189 | ||||
-rw-r--r-- | modules/bottom-sheet/ios/SheetViewController.swift | 76 | ||||
-rw-r--r-- | modules/bottom-sheet/ios/Util.swift | 18 |
6 files changed, 379 insertions, 0 deletions
diff --git a/modules/bottom-sheet/ios/BottomSheet.podspec b/modules/bottom-sheet/ios/BottomSheet.podspec new file mode 100644 index 000000000..a42356f61 --- /dev/null +++ b/modules/bottom-sheet/ios/BottomSheet.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'BottomSheet' + s.version = '1.0.0' + s.summary = 'A bottom sheet for use in Bluesky' + s.description = 'A bottom sheet for use in Bluesky' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '15.0', :tvos => '15.0' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,swift}" +end diff --git a/modules/bottom-sheet/ios/BottomSheetModule.swift b/modules/bottom-sheet/ios/BottomSheetModule.swift new file mode 100644 index 000000000..579608e75 --- /dev/null +++ b/modules/bottom-sheet/ios/BottomSheetModule.swift @@ -0,0 +1,47 @@ +import ExpoModulesCore + +public class BottomSheetModule: Module { + public func definition() -> ModuleDefinition { + Name("BottomSheet") + + AsyncFunction("dismissAll") { + SheetManager.shared.dismissAll() + } + + View(SheetView.self) { + Events([ + "onAttemptDismiss", + "onSnapPointChange", + "onStateChange" + ]) + + AsyncFunction("dismiss") { (view: SheetView) in + view.dismiss() + } + + AsyncFunction("updateLayout") { (view: SheetView) in + view.updateLayout() + } + + Prop("cornerRadius") { (view: SheetView, prop: Float) in + view.cornerRadius = CGFloat(prop) + } + + Prop("minHeight") { (view: SheetView, prop: Double) in + view.minHeight = prop + } + + Prop("maxHeight") { (view: SheetView, prop: Double) in + view.maxHeight = prop + } + + Prop("preventDismiss") { (view: SheetView, prop: Bool) in + view.preventDismiss = prop + } + + Prop("preventExpansion") { (view: SheetView, prop: Bool) in + view.preventExpansion = prop + } + } + } +} diff --git a/modules/bottom-sheet/ios/SheetManager.swift b/modules/bottom-sheet/ios/SheetManager.swift new file mode 100644 index 000000000..e4e843bea --- /dev/null +++ b/modules/bottom-sheet/ios/SheetManager.swift @@ -0,0 +1,28 @@ +// +// SheetManager.swift +// Pods +// +// Created by Hailey on 10/1/24. +// + +import ExpoModulesCore + +class SheetManager { + static let shared = SheetManager() + + private var sheetViews = NSHashTable<SheetView>(options: .weakMemory) + + func add(_ view: SheetView) { + sheetViews.add(view) + } + + func remove(_ view: SheetView) { + sheetViews.remove(view) + } + + func dismissAll() { + sheetViews.allObjects.forEach { sheetView in + sheetView.dismiss() + } + } +} diff --git a/modules/bottom-sheet/ios/SheetView.swift b/modules/bottom-sheet/ios/SheetView.swift new file mode 100644 index 000000000..cf2019c6a --- /dev/null +++ b/modules/bottom-sheet/ios/SheetView.swift @@ -0,0 +1,189 @@ +import ExpoModulesCore +import UIKit + +class SheetView: ExpoView, UISheetPresentationControllerDelegate { + // Views + private var sheetVc: SheetViewController? + private var innerView: UIView? + private var touchHandler: RCTTouchHandler? + + // Events + private let onAttemptDismiss = EventDispatcher() + private let onSnapPointChange = EventDispatcher() + private let onStateChange = EventDispatcher() + + // Open event firing + private var isOpen: Bool = false { + didSet { + onStateChange([ + "state": isOpen ? "open" : "closed" + ]) + } + } + + // React view props + var preventDismiss = false + var preventExpansion = false + var cornerRadius: CGFloat? + var minHeight = 0.0 + var maxHeight: CGFloat! { + didSet { + let screenHeight = Util.getScreenHeight() ?? 0 + if maxHeight > screenHeight { + maxHeight = screenHeight + } + } + } + + private var isOpening = false { + didSet { + if isOpening { + onStateChange([ + "state": "opening" + ]) + } + } + } + private var isClosing = false { + didSet { + if isClosing { + onStateChange([ + "state": "closing" + ]) + } + } + } + private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? { + didSet { + if selectedDetentIdentifier == .large { + onSnapPointChange([ + "snapPoint": 2 + ]) + } else { + onSnapPointChange([ + "snapPoint": 1 + ]) + } + } + } + + // MARK: - Lifecycle + + required init (appContext: AppContext? = nil) { + super.init(appContext: appContext) + self.maxHeight = Util.getScreenHeight() + self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge) + SheetManager.shared.add(self) + } + + deinit { + self.destroy() + } + + // We don't want this view to actually get added to the tree, so we'll simply store it for adding + // to the SheetViewController + override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { + self.touchHandler?.attach(to: subview) + self.innerView = subview + } + + // We'll grab the content height from here so we know the initial detent to set + override func layoutSubviews() { + super.layoutSubviews() + + guard let innerView = self.innerView else { + return + } + + if innerView.subviews.count != 1 { + return + } + + self.present() + } + + private func destroy() { + self.isClosing = false + self.isOpen = false + self.sheetVc = nil + self.touchHandler?.detach(from: self.innerView) + self.touchHandler = nil + self.innerView = nil + SheetManager.shared.remove(self) + } + + // MARK: - Presentation + + func present() { + guard !self.isOpen, + !self.isOpening, + !self.isClosing, + let innerView = self.innerView, + let contentHeight = innerView.subviews.first?.frame.height, + let rvc = self.reactViewController() else { + return + } + + let sheetVc = SheetViewController() + sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion) + if let sheet = sheetVc.sheetPresentationController { + sheet.delegate = self + sheet.preferredCornerRadius = self.cornerRadius + self.selectedDetentIdentifier = sheet.selectedDetentIdentifier + } + sheetVc.view.addSubview(innerView) + + self.sheetVc = sheetVc + self.isOpening = true + + rvc.present(sheetVc, animated: true) { [weak self] in + self?.isOpening = false + self?.isOpen = true + } + } + + func updateLayout() { + if let contentHeight = self.innerView?.subviews.first?.frame.size.height { + self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight), + preventExpansion: self.preventExpansion) + self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier() + } + } + + func dismiss() { + self.isClosing = true + self.sheetVc?.dismiss(animated: true) { [weak self] in + self?.destroy() + } + } + + // MARK: - Utils + + private func clampHeight(_ height: CGFloat) -> CGFloat { + if height < self.minHeight { + return self.minHeight + } else if height > self.maxHeight { + return self.maxHeight + } + return height + } + + // MARK: - UISheetPresentationControllerDelegate + + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { + self.onAttemptDismiss() + return !self.preventDismiss + } + + func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + self.isClosing = true + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.destroy() + } + + func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { + self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier + } +} diff --git a/modules/bottom-sheet/ios/SheetViewController.swift b/modules/bottom-sheet/ios/SheetViewController.swift new file mode 100644 index 000000000..56473b21c --- /dev/null +++ b/modules/bottom-sheet/ios/SheetViewController.swift @@ -0,0 +1,76 @@ +// +// SheetViewController.swift +// Pods +// +// Created by Hailey on 9/30/24. +// + +import Foundation +import UIKit + +class SheetViewController: UIViewController { + init() { + super.init(nibName: nil, bundle: nil) + + self.modalPresentationStyle = .formSheet + self.isModalInPresentation = false + + if let sheet = self.sheetPresentationController { + sheet.prefersGrabberVisible = false + } + } + + func setDetents(contentHeight: CGFloat, preventExpansion: Bool) { + guard let sheet = self.sheetPresentationController, + let screenHeight = Util.getScreenHeight() + else { + return + } + + if contentHeight > screenHeight - 100 { + sheet.detents = [ + .large() + ] + sheet.selectedDetentIdentifier = .large + } else { + if #available(iOS 16.0, *) { + sheet.detents = [ + .custom { _ in + return contentHeight + } + ] + } else { + sheet.detents = [ + .medium() + ] + } + + if !preventExpansion { + sheet.detents.append(.large()) + } + sheet.selectedDetentIdentifier = .medium + } + } + + func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) { + if let sheet = self.sheetPresentationController { + sheet.animateChanges { + self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion) + if #available(iOS 16.0, *) { + sheet.invalidateDetents() + } + } + } + } + + func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? { + guard let sheet = self.sheetPresentationController else { + return nil + } + return sheet.selectedDetentIdentifier + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/modules/bottom-sheet/ios/Util.swift b/modules/bottom-sheet/ios/Util.swift new file mode 100644 index 000000000..c654596a7 --- /dev/null +++ b/modules/bottom-sheet/ios/Util.swift @@ -0,0 +1,18 @@ +// +// Util.swift +// Pods +// +// Created by Hailey on 10/2/24. +// + +class Util { + static func getScreenHeight() -> CGFloat? { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + let safeAreaInsets = window.safeAreaInsets + let fullScreenHeight = UIScreen.main.bounds.height + return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom) + } + return nil + } +} |