Repository: Wouter125/BottomSheet
Branch: main
Commit: 83f016b6ad8e
Files: 37
Total size: 61.0 KB
Directory structure:
gitextract_vhrt5p88/
├── .gitignore
├── Example/
│ ├── BottomSheetExample/
│ │ ├── Apple Applications/
│ │ │ └── StocksExample.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── BottomSheetExampleApp.swift
│ │ ├── ExampleOverview.swift
│ │ ├── Examples/
│ │ │ └── StaticScrollViewExample.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ └── View Modifiers/
│ │ └── CornerRadius.swift
│ └── BottomSheetExample.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata/
│ └── xcschemes/
│ └── BottomSheetExample.xcscheme
├── LICENSE
├── Package.swift
├── README.md
├── Sources/
│ └── BottomSheet/
│ ├── Animation/
│ │ ├── Animation.swift
│ │ └── AnimationDefaults.swift
│ ├── BottomSheet.swift
│ ├── Detents/
│ │ ├── DetentDefaults.swift
│ │ ├── DetentHelpers.swift
│ │ └── Detents.swift
│ ├── Helpers/
│ │ ├── KeyboardReader.swift
│ │ └── Snapping.swift
│ ├── Preference Keys/
│ │ ├── BackgroundInteractionKey.swift
│ │ ├── ConfigKey.swift
│ │ └── IndicatorKey.swift
│ ├── UIKit Views/
│ │ └── UIScrollViewWrapper.swift
│ ├── View Modifiers/
│ │ ├── View+AnimationChange.swift
│ │ ├── View+BackgroundInteraction.swift
│ │ ├── View+Detents.swift
│ │ ├── View+DragIndicator.swift
│ │ └── View+SheetPlus.swift
│ └── Views/
│ └── DragIndicator.swift
└── Tests/
└── BottomSheetTests/
└── BottomSheetTests.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
================================================
FILE: Example/BottomSheetExample/Apple Applications/StocksExample.swift
================================================
//
// Stocks.swift
// BottomSheetExample
//
// Created by Wouter van de Kamp on 04/12/2022.
//
import SwiftUI
import BottomSheet
struct StocksExample: View {
@EnvironmentObject var settings: SheetSettings
var body: some View {
VStack {
Button("Close") {
settings.isPresented.toggle()
}
Button("Change") {
settings.selectedDetent = .large
}
Color.clear
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(settings.translation.rounded())")
.onAppear {
settings.isPresented = true
settings.activeSheetType = .stocks
}
}
}
}
struct StocksHeader: View {
var body: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Business News")
.font(.title)
.fontWeight(.heavy)
Text("From Yahoo Finance")
.foregroundColor(Color(UIColor.secondaryLabel))
}
.padding(.top, 10)
.padding(.bottom, 16)
Spacer()
}
Divider()
.frame(height: 1)
.background(Color(UIColor.systemGray6))
}
.padding(.top, 8)
.padding(.horizontal, 16)
}
}
struct StocksMainContent: View {
var body: some View {
VStack(spacing: 0) {
ScrollView {
ForEach(0..<5, id: \.self) { _ in
newsRow
}
}
Spacer(minLength: 72)
}
}
var newsRow: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("FX Empire")
.font(.caption)
.foregroundColor(Color(UIColor.secondaryLabel))
Text("Bitcoin (BTC) treads water after brief visit to sub-$39,000")
.font(.headline)
.foregroundColor(Color(UIColor.label))
Text("While Bitcoin (BTC) struggled on Saturday, XRP")
.foregroundColor(Color(UIColor.secondaryLabel))
.lineLimit(1)
}
.padding(.vertical, 16)
Spacer()
}
HStack {
Text("13h ago")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(Color(UIColor.secondaryLabel))
Spacer()
}
}
.padding(.horizontal, 16)
}
}
struct StocksExample_Previews: PreviewProvider {
static var previews: some View {
StocksExample()
}
}
================================================
FILE: Example/BottomSheetExample/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Example/BottomSheetExample/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Example/BottomSheetExample/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Example/BottomSheetExample/BottomSheetExampleApp.swift
================================================
//
// BottomSheetExampleApp.swift
// BottomSheetExample
//
// Created by Wouter van de Kamp on 10/03/2022.
//
import SwiftUI
@main
struct BottomSheetExampleApp: App {
var body: some Scene {
WindowGroup {
ExampleOverview()
}
}
}
================================================
FILE: Example/BottomSheetExample/ExampleOverview.swift
================================================
//
// ExampleOverview.swift
// BottomSheetExample
//
// Created by Wouter van de Kamp on 24/05/2022.
//
import SwiftUI
import BottomSheet
enum SheetExampleTypes {
case home
case stocks
case staticScrollView
}
class SheetSettings: ObservableObject {
@Published var isPresented = false
@Published var activeSheetType: SheetExampleTypes = .home
@Published var selectedDetent: BottomSheet.PresentationDetent = .medium
@Published var translation: CGFloat = BottomSheet.PresentationDetent.large.size
}
struct ExampleOverview: View {
@StateObject var settings = SheetSettings()
var views: [(label: String, view: AnyView)] = [
(label: "Stocks example", view: AnyView(StocksExample())),
(label: "Static scrollview example", view: AnyView(StaticScrollViewExample()))
]
@ViewBuilder
var headerContent: some View {
switch settings.activeSheetType {
case .stocks:
StocksHeader()
case .staticScrollView:
StaticScrollViewHeader()
default:
EmptyView()
}
}
@ViewBuilder
var mainContent: some View {
switch settings.activeSheetType {
case .stocks:
StocksMainContent()
.presentationDetentsPlus(
[.height(244), .medium, .large],
selection: $settings.selectedDetent
)
case .staticScrollView:
StaticScrollViewContent()
.presentationDetentsPlus(
[.height(244), .height(380), .height(480), .large],
selection: $settings.selectedDetent
)
.presentationDragIndicatorPlus(.visible)
.presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380)))
default:
EmptyView()
}
}
var body: some View {
ZStack {
NavigationView {
List(views.indices, id: \.self) { index in
NavigationLink(destination: views[index].view) {
Text(views[index].label)
}
}
.background(Color(UIColor.systemGroupedBackground))
.listStyle(.grouped)
.navigationTitle("Examples")
.onAppear {
settings.isPresented = false
settings.activeSheetType = .home
settings.selectedDetent = .medium
}
}
.navigationViewStyle(.stack)
}
.environmentObject(settings)
.sheetPlus(
isPresented: $settings.isPresented,
background: (
Color(UIColor.secondarySystemBackground)
.cornerRadius(12, corners: [.topLeft, .topRight])
),
onDrag: { translation in
settings.translation = translation
},
header: { headerContent },
main: {
mainContent
}
)
.overlay(
VStack(spacing: 0) {
Divider()
.frame(height: 1)
.background(Color(UIColor.systemGray6))
HStack {
Text("Yahoo Finance")
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 16)
}
.background(
Color(UIColor.secondarySystemBackground)
.edgesIgnoringSafeArea([.bottom])
)
.opacity(
settings.activeSheetType == .stocks ? 1 : 0
)
,
alignment: .bottom
)
}
}
struct ExampleOverview_Previews: PreviewProvider {
static var previews: some View {
ExampleOverview()
}
}
================================================
FILE: Example/BottomSheetExample/Examples/StaticScrollViewExample.swift
================================================
//
// StaticScrollViewExample.swift
// BottomSheetExample
//
// Created by Wouter van de Kamp on 28/10/2023.
//
import SwiftUI
struct StaticScrollViewExample: View {
@EnvironmentObject var settings: SheetSettings
var body: some View {
VStack {
Button("Close") {
settings.isPresented.toggle()
}
Button("Change") {
settings.selectedDetent = .large
}
Color.clear
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(settings.translation.rounded())")
.onAppear {
settings.isPresented = true
settings.activeSheetType = .staticScrollView
}
}
}
}
struct StaticScrollViewHeader: View {
@State private var searchterm = ""
var body: some View {
VStack {
TextField("Search item", text: $searchterm)
}
}
}
struct StaticScrollViewContent: View {
var body: some View {
ScrollView {
ForEach(0..<5, id: \.self) { idx in
Text("Item \(idx)")
}
}
}
}
struct StaticScrollViewExample_Previews: PreviewProvider {
static var previews: some View {
StaticScrollViewExample()
}
}
================================================
FILE: Example/BottomSheetExample/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Example/BottomSheetExample/View Modifiers/CornerRadius.swift
================================================
//
// CornerRadius.swift
// BottomSheetExample
//
// Created by Wouter van de Kamp on 25/03/2022.
//
import SwiftUI
struct CornerRadiusStyle: ViewModifier {
var radius: CGFloat
var corners: UIRectCorner
struct CornerRadiusShape: Shape {
var radius = CGFloat.infinity
var corners = UIRectCorner.allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
func body(content: Content) -> some View {
content
.clipShape(CornerRadiusShape(radius: radius, corners: corners))
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
ModifiedContent(content: self, modifier: CornerRadiusStyle(radius: radius, corners: corners))
}
}
================================================
FILE: Example/BottomSheetExample.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */; };
6C0BD47127DA862D000AF3CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47027DA862D000AF3CD /* Assets.xcassets */; };
6C0BD47427DA862D000AF3CD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */; };
6C6EB0B6292AEADC00106A1D /* BottomSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 6C6EB0B5292AEADC00106A1D /* BottomSheet */; };
6C763147283D774500463709 /* ExampleOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C763146283D774500463709 /* ExampleOverview.swift */; };
6C8CBF1D2AED12E00007E10E /* StaticScrollViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8CBF1C2AED12E00007E10E /* StaticScrollViewExample.swift */; };
6CDF5A0D27ED33C7004609F4 /* CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDF5A0C27ED33C7004609F4 /* CornerRadius.swift */; };
6CF78515293D36FB000E6581 /* StocksExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF78514293D36FB000E6581 /* StocksExample.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BottomSheetExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetExampleApp.swift; sourceTree = "<group>"; };
6C0BD47027DA862D000AF3CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
6C1DE0102889CF10003C6EE9 /* BottomSheet */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BottomSheet; path = ..; sourceTree = "<group>"; };
6C763146283D774500463709 /* ExampleOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleOverview.swift; sourceTree = "<group>"; };
6C8CBF1C2AED12E00007E10E /* StaticScrollViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticScrollViewExample.swift; sourceTree = "<group>"; };
6CDF5A0C27ED33C7004609F4 /* CornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadius.swift; sourceTree = "<group>"; };
6CF78514293D36FB000E6581 /* StocksExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StocksExample.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
6C0BD46627DA862D000AF3CD /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6C6EB0B6292AEADC00106A1D /* BottomSheet in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
6C0BD46027DA862C000AF3CD = {
isa = PBXGroup;
children = (
6C1DE0102889CF10003C6EE9 /* BottomSheet */,
6C0BD46B27DA862D000AF3CD /* BottomSheetExample */,
6C0BD46A27DA862D000AF3CD /* Products */,
6C0BD47A27DA87A1000AF3CD /* Frameworks */,
);
sourceTree = "<group>";
};
6C0BD46A27DA862D000AF3CD /* Products */ = {
isa = PBXGroup;
children = (
6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */,
);
name = Products;
sourceTree = "<group>";
};
6C0BD46B27DA862D000AF3CD /* BottomSheetExample */ = {
isa = PBXGroup;
children = (
6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */,
6C763146283D774500463709 /* ExampleOverview.swift */,
6CF78516293D3703000E6581 /* Apple Applications */,
6C8CBF1B2AED12C60007E10E /* Examples */,
6CDF5A0E27ED33D3004609F4 /* View Modifiers */,
6C0BD47027DA862D000AF3CD /* Assets.xcassets */,
6C0BD47227DA862D000AF3CD /* Preview Content */,
);
path = BottomSheetExample;
sourceTree = "<group>";
};
6C0BD47227DA862D000AF3CD /* Preview Content */ = {
isa = PBXGroup;
children = (
6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
6C0BD47A27DA87A1000AF3CD /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
6C8CBF1B2AED12C60007E10E /* Examples */ = {
isa = PBXGroup;
children = (
6C8CBF1C2AED12E00007E10E /* StaticScrollViewExample.swift */,
);
path = Examples;
sourceTree = "<group>";
};
6CDF5A0E27ED33D3004609F4 /* View Modifiers */ = {
isa = PBXGroup;
children = (
6CDF5A0C27ED33C7004609F4 /* CornerRadius.swift */,
);
path = "View Modifiers";
sourceTree = "<group>";
};
6CF78516293D3703000E6581 /* Apple Applications */ = {
isa = PBXGroup;
children = (
6CF78514293D36FB000E6581 /* StocksExample.swift */,
);
path = "Apple Applications";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
6C0BD46827DA862D000AF3CD /* BottomSheetExample */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6C0BD47727DA862D000AF3CD /* Build configuration list for PBXNativeTarget "BottomSheetExample" */;
buildPhases = (
6C0BD46527DA862D000AF3CD /* Sources */,
6C0BD46627DA862D000AF3CD /* Frameworks */,
6C939DBE294DFF9200F6EF50 /* Swiftlint */,
6C0BD46727DA862D000AF3CD /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = BottomSheetExample;
packageProductDependencies = (
6C6EB0B5292AEADC00106A1D /* BottomSheet */,
);
productName = BottomSheetExample;
productReference = 6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
6C0BD46127DA862C000AF3CD /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
TargetAttributes = {
6C0BD46827DA862D000AF3CD = {
CreatedOnToolsVersion = 13.2.1;
};
};
};
buildConfigurationList = 6C0BD46427DA862C000AF3CD /* Build configuration list for PBXProject "BottomSheetExample" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 6C0BD46027DA862C000AF3CD;
packageReferences = (
);
productRefGroup = 6C0BD46A27DA862D000AF3CD /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
6C0BD46827DA862D000AF3CD /* BottomSheetExample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
6C0BD46727DA862D000AF3CD /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6C0BD47427DA862D000AF3CD /* Preview Assets.xcassets in Resources */,
6C0BD47127DA862D000AF3CD /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6C939DBE294DFF9200F6EF50 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
6C0BD46527DA862D000AF3CD /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */,
6C8CBF1D2AED12E00007E10E /* StaticScrollViewExample.swift in Sources */,
6CDF5A0D27ED33C7004609F4 /* CornerRadius.swift in Sources */,
6C763147283D774500463709 /* ExampleOverview.swift in Sources */,
6CF78515293D36FB000E6581 /* StocksExample.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
6C0BD47527DA862D000AF3CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
6C0BD47627DA862D000AF3CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
6C0BD47827DA862D000AF3CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "BottomSheetExample/Preview\\ Content";
DEVELOPMENT_TEAM = KZAMEFAGHT;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.chargetrip.BottomSheetExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
6C0BD47927DA862D000AF3CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "BottomSheetExample/Preview\\ Content";
DEVELOPMENT_TEAM = KZAMEFAGHT;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.chargetrip.BottomSheetExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
6C0BD46427DA862C000AF3CD /* Build configuration list for PBXProject "BottomSheetExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6C0BD47527DA862D000AF3CD /* Debug */,
6C0BD47627DA862D000AF3CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
6C0BD47727DA862D000AF3CD /* Build configuration list for PBXNativeTarget "BottomSheetExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6C0BD47827DA862D000AF3CD /* Debug */,
6C0BD47927DA862D000AF3CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
6C6EB0B5292AEADC00106A1D /* BottomSheet */ = {
isa = XCSwiftPackageProductDependency;
productName = BottomSheet;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 6C0BD46127DA862C000AF3CD /* Project object */;
}
================================================
FILE: Example/BottomSheetExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
================================================
FILE: Example/BottomSheetExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
================================================
FILE: Example/BottomSheetExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
================================================
FILE: Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C0BD46827DA862D000AF3CD"
BuildableName = "BottomSheetExample.app"
BlueprintName = "BottomSheetExample"
ReferencedContainer = "container:BottomSheetExample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C0BD46827DA862D000AF3CD"
BuildableName = "BottomSheetExample.app"
BlueprintName = "BottomSheetExample"
ReferencedContainer = "container:BottomSheetExample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6C0BD46827DA862D000AF3CD"
BuildableName = "BottomSheetExample.app"
BlueprintName = "BottomSheetExample"
ReferencedContainer = "container:BottomSheetExample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Wouter van de Kamp.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "BottomSheet",
platforms: [
.iOS(.v14)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "BottomSheet",
targets: ["BottomSheet"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "BottomSheet",
dependencies: []),
.testTarget(
name: "BottomSheetTests",
dependencies: ["BottomSheet"]),
]
)
================================================
FILE: README.md
================================================
# BottomSheet
An iOS library for SwiftUI to create draggable sheet experiences similar to iOS applications like Maps and Stocks.
## Feature overview
The library currently supports;
- [x] Unlimited snap positions
- [x] Realtime position callback
- [x] Absolute and relative positioning
- [x] Customizable animation parameters
- [x] An optional sticky header
- [x] Views with and without a scrollview
## How to install
Currently BottomSheet is only available through the [Swift Package Manager](https://swift.org/package-manager/) or manual install.
1. Installation through Swift Package Manager can be done by going to `File > Add Packages`. Then enter the following URL in the searchbar; `https://github.com/Wouter125/BottomSheet`.
2. Manual installation can be done by cloning this repository and dragging all assets into your Xcode Project.
## How to use
1. Import BottomSheet
2. Create a state property that contains the presentation state of the bottom sheet and one for the current selection;
```
@Published var isPresented = false
@Published var selectedDetent: BottomSheet.PresentationDetent = .medium
```
4. Add the `BottomSheetView` to your SwiftUI view hierachy by using a view modifier;
```
.sheetPlus(
isPresented: $isPresented,
header: { },
main: {
EmptyView()
.presentationDetentsPlus(
[.height(244), .fraction(0.4), .medium, .large],
selection: $selectedDetent
)
}
)
```
5. Optionally receive the current panel position with a callback, change the background color, show a drag indicator or limit the background interaction based on the height;
```
.sheetPlus(
isPresented: $isPresented,
background: (
Color(UIColor.secondarySystemBackground)
),
onDrag: { translation in
print(translation)
},
header: { EmptyView() },
main: {
EmptyView()
.presentationDetentsPlus(
[.height(244), .fraction(0.4), .medium, .large],
selection: $selectedDetent
)
.presentationDragIndicatorPlus(.visible)
.presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380)))
}
)
```
## Interface
| Modifier | Type | Default | Description |
|--------------------------|---------------------|---------|-----------------------------------------------------------------------------------|
| animationCurve.mass | Double | 1.2 | The mass of the object attached to the spring. |
| animationCurve.stiffness | Double | 200 | The stiffness of the spring. |
| animationCurve.damping | Double | 25 | The spring damping value. |
## Example
To give you an idea of how to use this library you can use the example that is attached to this repo. Simply clone it and open the `BottomSheetExample` folder in Xcode.
## Roadmap
1. Add landscape support
2. Add iPad support
================================================
FILE: Sources/BottomSheet/Animation/Animation.swift
================================================
//
// Animation.swift
//
//
// Created by Wouter van de Kamp on 26/11/2022.
//
import Foundation
public struct SheetAnimation {
var mass: Double
var stiffness: Double
var damping: Double
public init(mass: Double, stiffness: Double, damping: Double) {
self.mass = mass
self.stiffness = stiffness
self.damping = damping
}
}
================================================
FILE: Sources/BottomSheet/Animation/AnimationDefaults.swift
================================================
//
// AnimationDefaults.swift
//
//
// Created by Wouter van de Kamp on 26/11/2022.
//
import Foundation
public struct SheetAnimationDefaults {
public static let mass: Double = 1.2
public static let stiffness: Double = 200
public static let damping: Double = 25
}
================================================
FILE: Sources/BottomSheet/BottomSheet.swift
================================================
//
// BottomSheet.swift
//
//
// Created by Wouter van de Kamp on 26/11/2022.
//
import SwiftUI
struct SheetPlus<HContent: View, MContent: View, Background: View>: ViewModifier, KeyboardReader {
@Binding private var isPresented: Bool
@State private var translation: CGFloat = 0
@State private var sheetConfig: SheetPlusConfig?
@State private var showDragIndicator: VisibilityPlus?
@State private var allowBackgroundInteraction: PresentationBackgroundInteractionPlus?
@State private var newValue = 0.0
@State private var startTime: DragGesture.Value?
@State private var detents: Set<PresentationDetent> = []
@State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0)
let mainContent: MContent
let headerContent: HContent
let animationCurve: SheetAnimation
let onDismiss: () -> Void
let onDrag: (CGFloat) -> Void
let background: Background
init(
isPresented: Binding<Bool>,
animationCurve: SheetAnimation,
background: Background,
onDismiss: @escaping () -> Void,
onDrag: @escaping (CGFloat) -> Void,
@ViewBuilder hcontent: () -> HContent,
@ViewBuilder mcontent: () -> MContent
) {
self._isPresented = isPresented
self.animationCurve = animationCurve
self.background = background
self.onDismiss = onDismiss
self.onDrag = onDrag
self.headerContent = hcontent()
self.mainContent = mcontent()
}
func body(content: Content) -> some View {
ZStack() {
content
.allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true)
if isPresented {
GeometryReader { geometry in
VStack(spacing: 0) {
// If / else statement here breaks the animation from the bottom level
// Might want to see if we can refactor the top level animation a bit
DragIndicator(
translation: $translation,
detents: detents
)
.frame(height: showDragIndicator == .visible ? 22 : 0)
.opacity(showDragIndicator == .visible ? 1 : 0)
headerContent
.contentShape(Rectangle())
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged { value in
translation -= value.location.y - value.startLocation.y - newValue
newValue = value.location.y - value.startLocation.y
if startTime == nil {
startTime = value
}
}
.onEnded { value in
// Reset the distance on release so we start with a
// clean translation next time
newValue = 0
// Calculate velocity based on pt/s so it matches the UIPanGesture
let distance: CGFloat = value.translation.height
let time: CGFloat = value.time.timeIntervalSince(startTime!.time)
let yVelocity: CGFloat = -1 * ((distance / time) / 1000)
startTime = nil
if let result = snapBottomSheet(translation, detents, yVelocity) {
translation = result.size
sheetConfig?.selectedDetent = result
}
}
)
UIScrollViewWrapper(
translation: $translation,
preferenceKey: $sheetConfig,
limits: limits,
detents: detents
) {
mainContent
.frame(width: geometry.size.width)
}
}
.background(background)
.frame(height:
(limits.max - geometry.safeAreaInsets.top) > 0
? limits.max - geometry.safeAreaInsets.top
: limits.max
)
.onChange(of: translation) { newValue in
// Small little hack to make the iOS scroll behaviour work smoothly
if limits.max == 0 { return }
translation = min(limits.max, max(newValue, limits.min))
currentGlobalTranslation = translation
}
.onAnimationChange(of: translation) { value in
onDrag(value)
}
.offset(y: UIScreen.main.bounds.height - translation)
.onDisappear {
translation = 0
detents = []
onDismiss()
}
.animation(
.interpolatingSpring(
mass: animationCurve.mass,
stiffness: animationCurve.stiffness,
damping: animationCurve.damping
)
)
}
.edgesIgnoringSafeArea([.bottom])
.transition(.move(edge: .bottom))
}
}
.onPreferenceChange(SheetPlusKey.self) { value in
/// Quick hack to prevent the scrollview from resetting the height when keyboard shows up.
/// Replace if the root cause has been located.
if value.detents.count == 0 { return }
sheetConfig = value
translation = value.translation
detents = value.detents
limits = detentLimits(detents: detents)
}
.onPreferenceChange(SheetPlusIndicatorKey.self) { value in
showDragIndicator = value
}
.onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in
allowBackgroundInteraction = value
}
}
}
================================================
FILE: Sources/BottomSheet/Detents/DetentDefaults.swift
================================================
//
// DetentsDefaults.swift
//
//
// Created by Wouter van de Kamp on 20/11/2022.
//
import SwiftUI
internal struct PresentationDetentDefaults {
static let small: CGFloat = UIScreen.main.bounds.height * 0.2
static let medium: CGFloat = UIScreen.main.bounds.height * 0.5
static let large: CGFloat = UIScreen.main.bounds.height * 0.9
}
================================================
FILE: Sources/BottomSheet/Detents/DetentHelpers.swift
================================================
//
// DetentsHelper.swift
//
//
// Created by Wouter van de Kamp on 20/11/2022.
//
import SwiftUI
/// Computes the limits of how far the sheet can move.
/// - Parameter detents: The list of detents provided when initializing the bottomsheet
/// - Returns: Tuple with top and bottom. Top reflects the offset from the top of the screen when the sheet is in it's largest form. Bottom reflects the offset from the top of the screen when the sheet is in it's smallest form.
internal func detentLimits(detents: Set<PresentationDetent>) -> (min: CGFloat, max: CGFloat) {
let detentLimits: [CGFloat] = detents
.map { detent in
switch detent {
case .small:
return PresentationDetentDefaults.small
case .medium:
return PresentationDetentDefaults.medium
case .large:
return PresentationDetentDefaults.large
case .fraction(let fraction):
return UIScreen.main.bounds.height * fraction
case .height(let height):
return height
}
}
.sorted(by: <)
return (min: detentLimits.first ?? 0, max: detentLimits.last ?? 0)
}
================================================
FILE: Sources/BottomSheet/Detents/Detents.swift
================================================
//
// Detents.swift
//
//
// Created by Wouter van de Kamp on 20/11/2022.
//
import SwiftUI
public enum PresentationDetent: Hashable {
case small
case medium
case large
case fraction(CGFloat)
case height(CGFloat)
public var size: CGFloat {
switch self {
case .small:
return PresentationDetentDefaults.small
case .medium:
return PresentationDetentDefaults.medium
case .large:
return PresentationDetentDefaults.large
case .fraction(let fraction):
return min(
UIScreen.main.bounds.height * fraction,
UIScreen.main.bounds.height
)
case .height(let height):
return min(
height,
UIScreen.main.bounds.height
)
}
}
}
================================================
FILE: Sources/BottomSheet/Helpers/KeyboardReader.swift
================================================
//
// KeyboardReader.swift
//
//
// Created by Wouter van de Kamp on 28/10/2023.
//
import Combine
import UIKit
protocol KeyboardReader {
var keyboardPublisher: AnyPublisher<Bool, Never> { get }
}
extension KeyboardReader {
var keyboardPublisher: AnyPublisher<Bool, Never> {
Publishers.Merge(
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { _ in true },
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in false }
)
.eraseToAnyPublisher()
}
}
================================================
FILE: Sources/BottomSheet/Helpers/Snapping.swift
================================================
//
// Snapping.swift
//
//
// Created by Wouter van de Kamp on 20/11/2022.
//
import Foundation
/// Helper function that computes where the bottomsheet should snap to
/// - Parameters:
/// - translation: the current translated distance
/// - detents: the detents the translation can snap to
/// - yVelocity: the speed at which the drag gesture ended. Used to compute a snapping behaviour
/// - Returns: The snapping position distance
internal func snapBottomSheet(_ translation: CGFloat, _ detents: Set<PresentationDetent>, _ yVelocity: CGFloat) -> PresentationDetent? {
let detents = detents.sorted(by: { $0.size < $1.size })
let position: [PresentationDetent] = detents.enumerated().compactMap { idx, detent in
if idx < detents.index(before: detents.count) {
let detentBracket = (
lower: detents[idx],
middle: detents[idx].size + ((detents[idx + 1].size - detents[idx].size) / 2),
upper: detents[idx + 1]
)
if detentBracket.lower.size...detentBracket.upper.size ~= translation {
if abs(yVelocity) > 1.8 {
return yVelocity > 0 ? detentBracket.upper : detentBracket.lower
} else {
return translation > detentBracket.middle
? detentBracket.upper
: detentBracket.lower
}
}
}
return nil
}
return position.first
}
================================================
FILE: Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift
================================================
//
// BackgroundInteractionKey.swift
//
//
// Created by Wouter van de Kamp on 29/10/2023.
//
import SwiftUI
// Currently using a global var.
// Might want to rework this by setting up the view modifiers a bit different.
// Probably something that we can hold translation in 1 var. Now both need to be in sync.
var currentGlobalTranslation: CGFloat = 0
public enum PresentationBackgroundInteractionPlus {
case automatic
case disabled
case enabled
public static func enabled(upThrough detent: PresentationDetent) -> PresentationBackgroundInteractionPlus {
currentGlobalTranslation > detent.size ? .disabled : .enabled
}
}
struct SheetPlusBackgroundInteractionKey: PreferenceKey {
static var defaultValue: PresentationBackgroundInteractionPlus = .automatic
static func reduce(value: inout PresentationBackgroundInteractionPlus, nextValue: () -> PresentationBackgroundInteractionPlus) {
value = nextValue()
}
}
================================================
FILE: Sources/BottomSheet/Preference Keys/ConfigKey.swift
================================================
//
// ConfigKey.swift
//
//
// Created by Wouter van de Kamp on 20/11/2022.
//
import SwiftUI
struct SheetPlusConfig: Equatable {
let detents: Set<PresentationDetent>
@Binding var selectedDetent: PresentationDetent
let translation: CGFloat
static func == (lhs: SheetPlusConfig, rhs: SheetPlusConfig) -> Bool {
return lhs.selectedDetent == rhs.selectedDetent && lhs.translation == rhs.translation && lhs.detents == rhs.detents
}
}
struct SheetPlusKey: PreferenceKey {
static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.height(.zero)), translation: 0)
static func reduce(value: inout SheetPlusConfig, nextValue: () -> SheetPlusConfig) {
/// This prevents the translation changes to be called whenever the keyboard is triggered.
/// If the keyboard gets triggered it will also reset the whole configkey and losing the binding.
/// https://stackoverflow.com/questions/67644164/preferencekey-issue-swiftui-sometimes-seems-to-generate-additional-views-that
value = nextValue() != defaultValue ? nextValue() : value
}
}
================================================
FILE: Sources/BottomSheet/Preference Keys/IndicatorKey.swift
================================================
//
// IndicatorKey.swift
//
//
// Created by Wouter van de Kamp on 29/10/2023.
//
import Foundation
import SwiftUI
struct SheetPlusIndicatorKey: PreferenceKey {
static var defaultValue: VisibilityPlus = .automatic
static func reduce(value: inout VisibilityPlus, nextValue: () -> VisibilityPlus) {
value = nextValue()
}
}
================================================
FILE: Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift
================================================
//
// UIScrollViewWrapper.swift
//
//
// Created by Wouter van de Kamp on 20/11/2022.
//
import Foundation
import SwiftUI
import UIKit
internal struct UIScrollViewWrapper<Content: View>: UIViewRepresentable {
@Binding var translation: CGFloat
@Binding var preferenceKey: SheetPlusConfig?
let limits: (min: CGFloat, max: CGFloat)
let detents: Set<PresentationDetent>
let content: () -> Content
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
let hostingController = context.coordinator.hostingController
scrollView.addSubview(hostingController.view)
scrollView.contentInsetAdjustmentBehavior = .automatic
scrollView.alwaysBounceVertical = true
scrollView.delegate = context.coordinator
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addConstraints([
hostingController.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
])
hostingController.view.backgroundColor = .clear
scrollView.backgroundColor = .clear
scrollView.layoutIfNeeded()
return scrollView
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
context.coordinator.limits = limits
context.coordinator.detents = detents
context.coordinator.hostingController.rootView = self.content()
}
func makeCoordinator() -> Coordinator {
return Coordinator(
representable: self,
hostingController: UIHostingController(rootView: content()),
limits: limits,
detents: detents
)
}
class Coordinator: NSObject, UIScrollViewDelegate {
private var scrollOffset: CGFloat = 0
private var newValue: CGFloat = 0
var representable: UIScrollViewWrapper
var hostingController: UIHostingController<Content>
var limits: (min: CGFloat, max: CGFloat)
var detents: Set<PresentationDetent>
init(
representable: UIScrollViewWrapper,
hostingController: UIHostingController<Content>,
limits: (min: CGFloat, max: CGFloat),
detents: Set<PresentationDetent>
) {
self.hostingController = hostingController
self.limits = limits
self.detents = detents
self.representable = representable
}
private func shouldDragSheet(_ scrollViewPosition: CGFloat, isFixedHeight: Bool) -> Bool {
// Translation on a scrollview without an overflow get's set to 0 somehow.
// Implemented this check to prevent it from snapping back to the original position.
// Need to dive deeper to figure out why it gets set to 0.
if isFixedHeight && representable.translation == 0 {
if scrollViewPosition > scrollOffset {
scrollOffset = scrollViewPosition
}
return scrollViewPosition < 0
}
if representable.translation >= limits.max {
if scrollViewPosition > scrollOffset {
scrollOffset = scrollViewPosition
}
return scrollViewPosition < 0
}
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let isFixedHeight = scrollView.contentSize.height < scrollView.frame.size.height
guard scrollView.isTracking else { return }
guard shouldDragSheet(scrollView.contentOffset.y, isFixedHeight: isFixedHeight) else {
scrollView.showsVerticalScrollIndicator = true
return
}
let localTranslation = scrollView.panGestureRecognizer.translation(in: scrollView.superview).y - scrollOffset
let translationDelta = localTranslation - newValue
representable.translation -= translationDelta
newValue = localTranslation
scrollView.showsVerticalScrollIndicator = false
scrollView.contentOffset.y = .zero
}
func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
if representable.translation != limits.max {
targetContentOffset.pointee = .zero
}
if let result = snapBottomSheet(
representable.translation,
detents,
scrollView.contentOffset.y > 0 ? 0 : velocity.y
) {
representable.translation = result.size
representable.preferenceKey?.selectedDetent = result
}
scrollOffset = 0
newValue = 0
}
}
}
================================================
FILE: Sources/BottomSheet/View Modifiers/View+AnimationChange.swift
================================================
//
// OnAnimationChange.swift
//
//
// Created by Wouter van de Kamp on 10/03/2022.
//
import SwiftUI
internal struct AnimationObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic {
var animatableData: Value {
didSet {
updateAnimationData()
}
}
private var update: (CGFloat) -> Void
init(observedValue: Value, update: @escaping (CGFloat) -> Void) {
self.animatableData = observedValue
self.update = update
}
func body(content: Content) -> some View {
return content
}
private func updateAnimationData() {
DispatchQueue.main.async {
// swiftlint:disable force_cast
update(animatableData as! CGFloat)
}
}
}
extension View {
func onAnimationChange<Value: VectorArithmetic>(
of value: Value,
perform: @escaping (CGFloat) -> Void
) -> ModifiedContent<Self, AnimationObserverModifier<Value>> {
return modifier(AnimationObserverModifier(observedValue: value, update: perform))
}
}
================================================
FILE: Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift
================================================
//
// View+BackgroundInteraction.swift
//
//
// Created by Wouter van de Kamp on 29/10/2023.
//
import SwiftUI
extension View {
public func presentationBackgroundInteractionPlus(
_ interaction: PresentationBackgroundInteractionPlus
) -> some View {
return self.preference(
key: SheetPlusBackgroundInteractionKey.self,
value: interaction
)
}
}
================================================
FILE: Sources/BottomSheet/View Modifiers/View+Detents.swift
================================================
//
// View+Detents.swift
//
//
// Created by Wouter van de Kamp on 02/07/2023.
//
import SwiftUI
extension View {
public func presentationDetentsPlus(
_ detents: Set<PresentationDetent>
) -> some View {
let sortedDetents = Array(detents).sorted(by: { $0.size < $1.size })
return self.preference(
key: SheetPlusKey.self,
value: SheetPlusConfig(
detents: detents,
selectedDetent: Binding(get: { sortedDetents.first! }, set: { _ in }),
translation: sortedDetents.first!.size
)
)
}
public func presentationDetentsPlus(
_ detents: Set<PresentationDetent>,
selection: Binding<PresentationDetent>
) -> some View {
return self.preference(
key: SheetPlusKey.self,
value: SheetPlusConfig(
detents: detents,
selectedDetent: selection,
translation: selection.wrappedValue.size
)
)
}
}
================================================
FILE: Sources/BottomSheet/View Modifiers/View+DragIndicator.swift
================================================
//
// View+DragIndicator.swift
//
//
// Created by Wouter van de Kamp on 29/10/2023.
//
import SwiftUI
public enum VisibilityPlus {
case hidden
case visible
case automatic
}
extension View {
public func presentationDragIndicatorPlus(
_ visibility: VisibilityPlus
) -> some View {
return self.preference(
key: SheetPlusIndicatorKey.self,
value: visibility
)
}
}
================================================
FILE: Sources/BottomSheet/View Modifiers/View+SheetPlus.swift
================================================
//
// View+SheetPlus.swift
//
//
// Created by Wouter van de Kamp on 21/11/2022.
//
import SwiftUI
extension View {
public func sheetPlus<HContent: View, MContent: View, Background: View>(
isPresented: Binding<Bool>,
animationCurve: SheetAnimation = SheetAnimation(
mass: SheetAnimationDefaults.mass,
stiffness: SheetAnimationDefaults.stiffness,
damping: SheetAnimationDefaults.damping
),
background: Background = Color(UIColor.systemBackground),
onDismiss: @escaping () -> Void = {},
onDrag: @escaping (CGFloat) -> Void = { _ in },
header: () -> HContent = { EmptyView() },
main: () -> MContent
) -> some View {
modifier(
SheetPlus(
isPresented: isPresented,
animationCurve: animationCurve,
background: background,
onDismiss: onDismiss,
onDrag: onDrag,
hcontent: header,
mcontent: main
)
)
}
}
================================================
FILE: Sources/BottomSheet/Views/DragIndicator.swift
================================================
//
// DragIndicator.swift
//
//
// Created by Wouter van de Kamp on 29/10/2023.
//
import SwiftUI
struct DragIndicator: View {
@Binding var translation: CGFloat
var detents: Set<PresentationDetent>
var body: some View {
RoundedRectangle(cornerRadius: 3)
.contentShape(Rectangle())
.frame(width: 42, height: 6)
.foregroundColor(Color(UIColor.systemGray3))
.padding(.vertical, 8)
.onTapGesture {
let sortedDetents = detents.sorted { $0.size < $1.size }
let nextDetent = sortedDetents.first(where: { $0.size > translation })
if let nextDetent = nextDetent {
translation = nextDetent.size
} else {
translation = sortedDetents.first!.size
}
}
}
}
struct DragIndicator_Previews: PreviewProvider {
static var previews: some View {
DragIndicator(
translation: .constant(0),
detents: []
)
}
}
================================================
FILE: Tests/BottomSheetTests/BottomSheetTests.swift
================================================
import XCTest
@testable import BottomSheet
final class BottomSheetTests: XCTestCase {
func testExample() throws {
}
}
gitextract_vhrt5p88/
├── .gitignore
├── Example/
│ ├── BottomSheetExample/
│ │ ├── Apple Applications/
│ │ │ └── StocksExample.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── BottomSheetExampleApp.swift
│ │ ├── ExampleOverview.swift
│ │ ├── Examples/
│ │ │ └── StaticScrollViewExample.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ └── View Modifiers/
│ │ └── CornerRadius.swift
│ └── BottomSheetExample.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata/
│ └── xcschemes/
│ └── BottomSheetExample.xcscheme
├── LICENSE
├── Package.swift
├── README.md
├── Sources/
│ └── BottomSheet/
│ ├── Animation/
│ │ ├── Animation.swift
│ │ └── AnimationDefaults.swift
│ ├── BottomSheet.swift
│ ├── Detents/
│ │ ├── DetentDefaults.swift
│ │ ├── DetentHelpers.swift
│ │ └── Detents.swift
│ ├── Helpers/
│ │ ├── KeyboardReader.swift
│ │ └── Snapping.swift
│ ├── Preference Keys/
│ │ ├── BackgroundInteractionKey.swift
│ │ ├── ConfigKey.swift
│ │ └── IndicatorKey.swift
│ ├── UIKit Views/
│ │ └── UIScrollViewWrapper.swift
│ ├── View Modifiers/
│ │ ├── View+AnimationChange.swift
│ │ ├── View+BackgroundInteraction.swift
│ │ ├── View+Detents.swift
│ │ ├── View+DragIndicator.swift
│ │ └── View+SheetPlus.swift
│ └── Views/
│ └── DragIndicator.swift
└── Tests/
└── BottomSheetTests/
└── BottomSheetTests.swift
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (69K chars).
[
{
"path": ".gitignore",
"chars": 126,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\nDerivedData/\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspac"
},
{
"path": "Example/BottomSheetExample/Apple Applications/StocksExample.swift",
"chars": 2908,
"preview": "//\n// Stocks.swift\n// BottomSheetExample\n//\n// Created by Wouter van de Kamp on 04/12/2022.\n//\n\nimport SwiftUI\nimport"
},
{
"path": "Example/BottomSheetExample/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 123,
"preview": "{\n \"colors\" : [\n {\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }"
},
{
"path": "Example/BottomSheetExample/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1591,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"iphone\",\n \"scale\" : \"2x\",\n \"size\" : \"20x20\"\n },\n {\n \"idiom\""
},
{
"path": "Example/BottomSheetExample/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Example/BottomSheetExample/BottomSheetExampleApp.swift",
"chars": 269,
"preview": "//\n// BottomSheetExampleApp.swift\n// BottomSheetExample\n//\n// Created by Wouter van de Kamp on 10/03/2022.\n//\n\nimport"
},
{
"path": "Example/BottomSheetExample/ExampleOverview.swift",
"chars": 3883,
"preview": "//\n// ExampleOverview.swift\n// BottomSheetExample\n//\n// Created by Wouter van de Kamp on 24/05/2022.\n//\n\nimport Swift"
},
{
"path": "Example/BottomSheetExample/Examples/StaticScrollViewExample.swift",
"chars": 1317,
"preview": "//\n// StaticScrollViewExample.swift\n// BottomSheetExample\n//\n// Created by Wouter van de Kamp on 28/10/2023.\n//\n\nimpo"
},
{
"path": "Example/BottomSheetExample/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Example/BottomSheetExample/View Modifiers/CornerRadius.swift",
"chars": 990,
"preview": "//\n// CornerRadius.swift\n// BottomSheetExample\n//\n// Created by Wouter van de Kamp on 25/03/2022.\n//\n\nimport SwiftUI\n"
},
{
"path": "Example/BottomSheetExample.xcodeproj/project.pbxproj",
"chars": 16987,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 55;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Example/BottomSheetExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "Example/BottomSheetExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Example/BottomSheetExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
"chars": 226,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme",
"chars": 2939,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1410\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "LICENSE",
"chars": 1075,
"preview": "MIT License\n\nCopyright (c) 2024 Wouter van de Kamp.\n\nPermission is hereby granted, free of charge, to any person obtaini"
},
{
"path": "Package.swift",
"chars": 1071,
"preview": "// swift-tools-version:5.5\n// The swift-tools-version declares the minimum version of Swift required to build this packa"
},
{
"path": "README.md",
"chars": 3195,
"preview": "# BottomSheet\n\nAn iOS library for SwiftUI to create draggable sheet experiences similar to iOS applications like Maps an"
},
{
"path": "Sources/BottomSheet/Animation/Animation.swift",
"chars": 377,
"preview": "//\n// Animation.swift\n// \n//\n// Created by Wouter van de Kamp on 26/11/2022.\n//\n\nimport Foundation\n\npublic struct She"
},
{
"path": "Sources/BottomSheet/Animation/AnimationDefaults.swift",
"chars": 283,
"preview": "//\n// AnimationDefaults.swift\n// \n//\n// Created by Wouter van de Kamp on 26/11/2022.\n//\n\nimport Foundation\n\npublic st"
},
{
"path": "Sources/BottomSheet/BottomSheet.swift",
"chars": 7007,
"preview": "//\n// BottomSheet.swift\n//\n//\n// Created by Wouter van de Kamp on 26/11/2022.\n//\n\nimport SwiftUI\n\nstruct SheetPlus<HCo"
},
{
"path": "Sources/BottomSheet/Detents/DetentDefaults.swift",
"chars": 352,
"preview": "//\n// DetentsDefaults.swift\n// \n//\n// Created by Wouter van de Kamp on 20/11/2022.\n//\n\nimport SwiftUI\n\ninternal struc"
},
{
"path": "Sources/BottomSheet/Detents/DetentHelpers.swift",
"chars": 1207,
"preview": "//\n// DetentsHelper.swift\n// \n//\n// Created by Wouter van de Kamp on 20/11/2022.\n//\n\nimport SwiftUI\n\n/// Computes the"
},
{
"path": "Sources/BottomSheet/Detents/Detents.swift",
"chars": 848,
"preview": "//\n// Detents.swift\n// \n//\n// Created by Wouter van de Kamp on 20/11/2022.\n//\n\nimport SwiftUI\n\npublic enum Presentati"
},
{
"path": "Sources/BottomSheet/Helpers/KeyboardReader.swift",
"chars": 664,
"preview": "//\n// KeyboardReader.swift\n// \n//\n// Created by Wouter van de Kamp on 28/10/2023.\n//\n\nimport Combine\nimport UIKit\n\npr"
},
{
"path": "Sources/BottomSheet/Helpers/Snapping.swift",
"chars": 1530,
"preview": "//\n// Snapping.swift\n// \n//\n// Created by Wouter van de Kamp on 20/11/2022.\n//\n\nimport Foundation\n\n/// Helper functio"
},
{
"path": "Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift",
"chars": 966,
"preview": "//\n// BackgroundInteractionKey.swift\n// \n//\n// Created by Wouter van de Kamp on 29/10/2023.\n//\n\nimport SwiftUI\n\n// Cu"
},
{
"path": "Sources/BottomSheet/Preference Keys/ConfigKey.swift",
"chars": 1155,
"preview": "//\n// ConfigKey.swift\n// \n//\n// Created by Wouter van de Kamp on 20/11/2022.\n//\n\nimport SwiftUI\n\nstruct SheetPlusConf"
},
{
"path": "Sources/BottomSheet/Preference Keys/IndicatorKey.swift",
"chars": 348,
"preview": "//\n// IndicatorKey.swift\n// \n//\n// Created by Wouter van de Kamp on 29/10/2023.\n//\n\nimport Foundation\nimport SwiftUI\n"
},
{
"path": "Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift",
"chars": 5302,
"preview": "//\n// UIScrollViewWrapper.swift\n// \n//\n// Created by Wouter van de Kamp on 20/11/2022.\n//\n\nimport Foundation\nimport S"
},
{
"path": "Sources/BottomSheet/View Modifiers/View+AnimationChange.swift",
"chars": 1069,
"preview": "//\n// OnAnimationChange.swift\n// \n//\n// Created by Wouter van de Kamp on 10/03/2022.\n//\n\nimport SwiftUI\n\ninternal str"
},
{
"path": "Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift",
"chars": 409,
"preview": "//\n// View+BackgroundInteraction.swift\n// \n//\n// Created by Wouter van de Kamp on 29/10/2023.\n//\n\nimport SwiftUI\n\next"
},
{
"path": "Sources/BottomSheet/View Modifiers/View+Detents.swift",
"chars": 1054,
"preview": "//\n// View+Detents.swift\n// \n//\n// Created by Wouter van de Kamp on 02/07/2023.\n//\n\nimport SwiftUI\n\nextension View { "
},
{
"path": "Sources/BottomSheet/View Modifiers/View+DragIndicator.swift",
"chars": 440,
"preview": "//\n// View+DragIndicator.swift\n// \n//\n// Created by Wouter van de Kamp on 29/10/2023.\n//\n\nimport SwiftUI\n\npublic enum"
},
{
"path": "Sources/BottomSheet/View Modifiers/View+SheetPlus.swift",
"chars": 1065,
"preview": "//\n// View+SheetPlus.swift\n// \n//\n// Created by Wouter van de Kamp on 21/11/2022.\n//\n\nimport SwiftUI\n\nextension View "
},
{
"path": "Sources/BottomSheet/Views/DragIndicator.swift",
"chars": 1057,
"preview": "//\n// DragIndicator.swift\n// \n//\n// Created by Wouter van de Kamp on 29/10/2023.\n//\n\nimport SwiftUI\n\nstruct DragIndic"
},
{
"path": "Tests/BottomSheetTests/BottomSheetTests.swift",
"chars": 136,
"preview": "import XCTest\n@testable import BottomSheet\n\nfinal class BottomSheetTests: XCTestCase {\n func testExample() throws {\n "
}
]
About this extraction
This page contains the full source code of the Wouter125/BottomSheet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (61.0 KB), approximately 16.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.