move into repo
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,46 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class BottomSheetModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("BottomSheet")
|
||||
|
||||
AsyncFunction("dismissAll") {
|
||||
SheetManager.shared.dismissAll()
|
||||
}
|
||||
|
||||
View(SheetView.self) {
|
||||
Events([
|
||||
"onStateChange",
|
||||
"onAttemptDismiss"
|
||||
])
|
||||
|
||||
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("preventDismiss") { (view: SheetView, prop: Bool) in
|
||||
view.preventDismiss = prop
|
||||
}
|
||||
|
||||
Prop("minHeight") { (view: SheetView, prop: Double) in
|
||||
view.minHeight = prop
|
||||
}
|
||||
|
||||
Prop("maxHeight") { (view: SheetView, prop: Double) in
|
||||
view.maxHeight = prop
|
||||
}
|
||||
|
||||
Prop("preventExpansion") { (view: SheetView, prop: Bool) in
|
||||
view.preventExpansion = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import ExpoModulesCore
|
||||
import UIKit
|
||||
|
||||
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
// Views
|
||||
private var sheetVc: SheetViewController?
|
||||
private var innerView: UIView?
|
||||
|
||||
// Events
|
||||
private let onStateChange = EventDispatcher()
|
||||
private let onAttemptDismiss = 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"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
required init (appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
self.maxHeight = Util.getScreenHeight()
|
||||
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.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(contentHeight: innerView.subviews[0].frame.size.height)
|
||||
}
|
||||
|
||||
private func destroy() {
|
||||
self.isClosing = false
|
||||
self.isOpen = false
|
||||
self.sheetVc = nil
|
||||
self.innerView = nil
|
||||
SheetManager.shared.remove(self)
|
||||
}
|
||||
|
||||
// MARK: - Presentation
|
||||
|
||||
func present(contentHeight: CGFloat) {
|
||||
guard !self.isOpen,
|
||||
let innerView = self.innerView,
|
||||
let rvc = self.reactViewController() else {
|
||||
return
|
||||
}
|
||||
|
||||
let sheetVc = SheetViewController()
|
||||
if let sheet = sheetVc.sheetPresentationController {
|
||||
sheet.delegate = self
|
||||
sheet.preferredCornerRadius = self.cornerRadius
|
||||
}
|
||||
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
||||
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[0].frame.size.height {
|
||||
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
|
||||
preventExpansion: self.preventExpansion)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// 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 = true
|
||||
}
|
||||
}
|
||||
|
||||
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||
guard let sheet = self.sheetPresentationController,
|
||||
let screenHeight = Util.getScreenHeight()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if contentHeight > screenHeight {
|
||||
sheet.detents = [
|
||||
.large()
|
||||
]
|
||||
} else {
|
||||
if #available(iOS 16.0, *) {
|
||||
sheet.detents = [
|
||||
.custom { _ in
|
||||
return contentHeight
|
||||
}
|
||||
]
|
||||
} else {
|
||||
sheet.detents = [
|
||||
.medium()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if !preventExpansion {
|
||||
sheet.detents.append(.large())
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user