├── .gitignore ├── JXBottomSheetView.podspec ├── JXBottomSheetView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── jiaxin.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── JXBottomSheetView ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Gif │ ├── Changed.gif │ ├── NormalScroll.gif │ └── Scroll.gif ├── Info.plist └── ViewController.swift ├── LICENSE ├── README.md └── Sources └── JXBottomSheetView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /JXBottomSheetView.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = "JXBottomSheetView" 4 | s.version = "0.0.6" 5 | s.summary = "可以手势交互的底部列表视图" 6 | s.homepage = "https://github.com/pujiaxin33/JXBottomSheetView" 7 | s.license = "MIT" 8 | s.author = { "pujiaxin33" => "317437084@qq.com" } 9 | s.platform = :ios, "9.0" 10 | s.swift_version = "5.0" 11 | s.source = { :git => "https://github.com/pujiaxin33/JXBottomSheetView.git", :tag => "#{s.version}" } 12 | s.framework = "UIKit" 13 | s.source_files = "Sources", "Sources/*.{swift}" 14 | s.requires_arc = true 15 | end 16 | -------------------------------------------------------------------------------- /JXBottomSheetView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1939226A218C4F2900C7B564 /* JXBottomSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19392269218C4F2900C7B564 /* JXBottomSheetView.swift */; }; 11 | 19985AB221119B20000DF69B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19985AB121119B20000DF69B /* AppDelegate.swift */; }; 12 | 19985AB421119B21000DF69B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19985AB321119B21000DF69B /* ViewController.swift */; }; 13 | 19985AB721119B21000DF69B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 19985AB521119B21000DF69B /* Main.storyboard */; }; 14 | 19985AB921119B23000DF69B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19985AB821119B23000DF69B /* Assets.xcassets */; }; 15 | 19985ABC21119B23000DF69B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 19985ABA21119B23000DF69B /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 19392269218C4F2900C7B564 /* JXBottomSheetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JXBottomSheetView.swift; sourceTree = ""; }; 20 | 19985AAE21119B20000DF69B /* JXBottomSheetView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JXBottomSheetView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 19985AB121119B20000DF69B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 19985AB321119B21000DF69B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 19985AB621119B21000DF69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 19985AB821119B23000DF69B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 19985ABB21119B23000DF69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 19985ABD21119B23000DF69B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 19985AAB21119B20000DF69B /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 19392268218C4F2900C7B564 /* Sources */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 19392269218C4F2900C7B564 /* JXBottomSheetView.swift */, 44 | ); 45 | path = Sources; 46 | sourceTree = SOURCE_ROOT; 47 | }; 48 | 19985AA521119B20000DF69B = { 49 | isa = PBXGroup; 50 | children = ( 51 | 19985AB021119B20000DF69B /* JXBottomSheetView */, 52 | 19985AAF21119B20000DF69B /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 19985AAF21119B20000DF69B /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 19985AAE21119B20000DF69B /* JXBottomSheetView.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 19985AB021119B20000DF69B /* JXBottomSheetView */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 19392268218C4F2900C7B564 /* Sources */, 68 | 19985AB121119B20000DF69B /* AppDelegate.swift */, 69 | 19985AB321119B21000DF69B /* ViewController.swift */, 70 | 19985AB521119B21000DF69B /* Main.storyboard */, 71 | 19985AB821119B23000DF69B /* Assets.xcassets */, 72 | 19985ABA21119B23000DF69B /* LaunchScreen.storyboard */, 73 | 19985ABD21119B23000DF69B /* Info.plist */, 74 | ); 75 | path = JXBottomSheetView; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | 19985AAD21119B20000DF69B /* JXBottomSheetView */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = 19985AC021119B23000DF69B /* Build configuration list for PBXNativeTarget "JXBottomSheetView" */; 84 | buildPhases = ( 85 | 19985AAA21119B20000DF69B /* Sources */, 86 | 19985AAB21119B20000DF69B /* Frameworks */, 87 | 19985AAC21119B20000DF69B /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = JXBottomSheetView; 94 | productName = JXBottomSheetView; 95 | productReference = 19985AAE21119B20000DF69B /* JXBottomSheetView.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | 19985AA621119B20000DF69B /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | LastSwiftUpdateCheck = 0940; 105 | LastUpgradeCheck = 0940; 106 | ORGANIZATIONNAME = jiaxin; 107 | TargetAttributes = { 108 | 19985AAD21119B20000DF69B = { 109 | CreatedOnToolsVersion = 9.4; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = 19985AA921119B20000DF69B /* Build configuration list for PBXProject "JXBottomSheetView" */; 114 | compatibilityVersion = "Xcode 9.3"; 115 | developmentRegion = en; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | Base, 120 | ); 121 | mainGroup = 19985AA521119B20000DF69B; 122 | productRefGroup = 19985AAF21119B20000DF69B /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | 19985AAD21119B20000DF69B /* JXBottomSheetView */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXResourcesBuildPhase section */ 132 | 19985AAC21119B20000DF69B /* Resources */ = { 133 | isa = PBXResourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 19985ABC21119B23000DF69B /* LaunchScreen.storyboard in Resources */, 137 | 19985AB921119B23000DF69B /* Assets.xcassets in Resources */, 138 | 19985AB721119B21000DF69B /* Main.storyboard in Resources */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXResourcesBuildPhase section */ 143 | 144 | /* Begin PBXSourcesBuildPhase section */ 145 | 19985AAA21119B20000DF69B /* Sources */ = { 146 | isa = PBXSourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 19985AB421119B21000DF69B /* ViewController.swift in Sources */, 150 | 1939226A218C4F2900C7B564 /* JXBottomSheetView.swift in Sources */, 151 | 19985AB221119B20000DF69B /* AppDelegate.swift in Sources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXSourcesBuildPhase section */ 156 | 157 | /* Begin PBXVariantGroup section */ 158 | 19985AB521119B21000DF69B /* Main.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | 19985AB621119B21000DF69B /* Base */, 162 | ); 163 | name = Main.storyboard; 164 | sourceTree = ""; 165 | }; 166 | 19985ABA21119B23000DF69B /* LaunchScreen.storyboard */ = { 167 | isa = PBXVariantGroup; 168 | children = ( 169 | 19985ABB21119B23000DF69B /* Base */, 170 | ); 171 | name = LaunchScreen.storyboard; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXVariantGroup section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | 19985ABE21119B23000DF69B /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_ANALYZER_NONNULL = YES; 182 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 184 | CLANG_CXX_LIBRARY = "libc++"; 185 | CLANG_ENABLE_MODULES = YES; 186 | CLANG_ENABLE_OBJC_ARC = YES; 187 | CLANG_ENABLE_OBJC_WEAK = YES; 188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 189 | CLANG_WARN_BOOL_CONVERSION = YES; 190 | CLANG_WARN_COMMA = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INFINITE_RECURSION = YES; 198 | CLANG_WARN_INT_CONVERSION = YES; 199 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 204 | CLANG_WARN_STRICT_PROTOTYPES = YES; 205 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 206 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 207 | CLANG_WARN_UNREACHABLE_CODE = YES; 208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 209 | CODE_SIGN_IDENTITY = "iPhone Developer"; 210 | COPY_PHASE_STRIP = NO; 211 | DEBUG_INFORMATION_FORMAT = dwarf; 212 | ENABLE_STRICT_OBJC_MSGSEND = YES; 213 | ENABLE_TESTABILITY = YES; 214 | GCC_C_LANGUAGE_STANDARD = gnu11; 215 | GCC_DYNAMIC_NO_PIC = NO; 216 | GCC_NO_COMMON_BLOCKS = YES; 217 | GCC_OPTIMIZATION_LEVEL = 0; 218 | GCC_PREPROCESSOR_DEFINITIONS = ( 219 | "DEBUG=1", 220 | "$(inherited)", 221 | ); 222 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 223 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 224 | GCC_WARN_UNDECLARED_SELECTOR = YES; 225 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 226 | GCC_WARN_UNUSED_FUNCTION = YES; 227 | GCC_WARN_UNUSED_VARIABLE = YES; 228 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 229 | MTL_ENABLE_DEBUG_INFO = YES; 230 | ONLY_ACTIVE_ARCH = YES; 231 | SDKROOT = iphoneos; 232 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 233 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 234 | }; 235 | name = Debug; 236 | }; 237 | 19985ABF21119B23000DF69B /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | CODE_SIGN_IDENTITY = "iPhone Developer"; 270 | COPY_PHASE_STRIP = NO; 271 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 272 | ENABLE_NS_ASSERTIONS = NO; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | GCC_C_LANGUAGE_STANDARD = gnu11; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 277 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 278 | GCC_WARN_UNDECLARED_SELECTOR = YES; 279 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 280 | GCC_WARN_UNUSED_FUNCTION = YES; 281 | GCC_WARN_UNUSED_VARIABLE = YES; 282 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 283 | MTL_ENABLE_DEBUG_INFO = NO; 284 | SDKROOT = iphoneos; 285 | SWIFT_COMPILATION_MODE = wholemodule; 286 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 287 | VALIDATE_PRODUCT = YES; 288 | }; 289 | name = Release; 290 | }; 291 | 19985AC121119B23000DF69B /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 295 | CODE_SIGN_IDENTITY = "iPhone Developer"; 296 | CODE_SIGN_STYLE = Automatic; 297 | DEVELOPMENT_TEAM = CUHY5967Q2; 298 | INFOPLIST_FILE = JXBottomSheetView/Info.plist; 299 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 300 | LD_RUNPATH_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "@executable_path/Frameworks", 303 | ); 304 | PRODUCT_BUNDLE_IDENTIFIER = jiaxin.JXBottomSheetView; 305 | PRODUCT_NAME = "$(TARGET_NAME)"; 306 | PROVISIONING_PROFILE_SPECIFIER = ""; 307 | SWIFT_VERSION = 5.0; 308 | TARGETED_DEVICE_FAMILY = 1; 309 | }; 310 | name = Debug; 311 | }; 312 | 19985AC221119B23000DF69B /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | CODE_SIGN_IDENTITY = "iPhone Developer"; 317 | CODE_SIGN_STYLE = Automatic; 318 | DEVELOPMENT_TEAM = CUHY5967Q2; 319 | INFOPLIST_FILE = JXBottomSheetView/Info.plist; 320 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 321 | LD_RUNPATH_SEARCH_PATHS = ( 322 | "$(inherited)", 323 | "@executable_path/Frameworks", 324 | ); 325 | PRODUCT_BUNDLE_IDENTIFIER = jiaxin.JXBottomSheetView; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | PROVISIONING_PROFILE_SPECIFIER = ""; 328 | SWIFT_VERSION = 5.0; 329 | TARGETED_DEVICE_FAMILY = 1; 330 | }; 331 | name = Release; 332 | }; 333 | /* End XCBuildConfiguration section */ 334 | 335 | /* Begin XCConfigurationList section */ 336 | 19985AA921119B20000DF69B /* Build configuration list for PBXProject "JXBottomSheetView" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | 19985ABE21119B23000DF69B /* Debug */, 340 | 19985ABF21119B23000DF69B /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | defaultConfigurationName = Release; 344 | }; 345 | 19985AC021119B23000DF69B /* Build configuration list for PBXNativeTarget "JXBottomSheetView" */ = { 346 | isa = XCConfigurationList; 347 | buildConfigurations = ( 348 | 19985AC121119B23000DF69B /* Debug */, 349 | 19985AC221119B23000DF69B /* Release */, 350 | ); 351 | defaultConfigurationIsVisible = 0; 352 | defaultConfigurationName = Release; 353 | }; 354 | /* End XCConfigurationList section */ 355 | }; 356 | rootObject = 19985AA621119B20000DF69B /* Project object */; 357 | } 358 | -------------------------------------------------------------------------------- /JXBottomSheetView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JXBottomSheetView.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JXBottomSheetView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | JXBottomSheetView.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /JXBottomSheetView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // JXBottomSheetView 4 | // 5 | // Created by jiaxin on 2018/8/1. 6 | // Copyright © 2018年 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /JXBottomSheetView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /JXBottomSheetView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /JXBottomSheetView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /JXBottomSheetView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /JXBottomSheetView/Gif/Changed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXBottomSheetView/7bc7ef4b24c0afff445bc74fd8933a56c7db6540/JXBottomSheetView/Gif/Changed.gif -------------------------------------------------------------------------------- /JXBottomSheetView/Gif/NormalScroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXBottomSheetView/7bc7ef4b24c0afff445bc74fd8933a56c7db6540/JXBottomSheetView/Gif/NormalScroll.gif -------------------------------------------------------------------------------- /JXBottomSheetView/Gif/Scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXBottomSheetView/7bc7ef4b24c0afff445bc74fd8933a56c7db6540/JXBottomSheetView/Gif/Scroll.gif -------------------------------------------------------------------------------- /JXBottomSheetView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /JXBottomSheetView/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // JXBottomSheetView 4 | // 5 | // Created by jiaxin on 2018/8/1. 6 | // Copyright © 2018年 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | var tableView: UITableView! 13 | var dataSource: [String]! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | view.backgroundColor = UIColor.lightGray 18 | 19 | self.navigationItem.leftBarButtonItems = [ 20 | UIBarButtonItem(title: "AddDish", style: .plain, target: self, action: #selector(addDish)), 21 | UIBarButtonItem(title: "DeleteDish", style: .plain, target: self, action: #selector(deleteDish)) 22 | ] 23 | 24 | dataSource = ["回锅肉", "青椒肉丝", "麻婆豆腐", "火锅", "冷串串", "凉粉", "剁椒鱼头", "酸菜鱼", "锅盔", "天蚕土豆", "春卷"] 25 | 26 | tableView = UITableView.init(frame: CGRect.zero, style: .plain) 27 | tableView.showsVerticalScrollIndicator = false 28 | tableView.showsHorizontalScrollIndicator = false 29 | tableView.separatorStyle = .none 30 | tableView.delegate = self 31 | tableView.dataSource = self 32 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 33 | 34 | let bottomSheet = JXBottomSheetView(contentView: tableView) 35 | bottomSheet.defaultMininumDisplayHeight = 100 36 | bottomSheet.defaultMaxinumDisplayHeight = 300 37 | bottomSheet.displayState = .minDisplay 38 | bottomSheet.frame = self.view.bounds 39 | view.addSubview(bottomSheet) 40 | } 41 | 42 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 43 | print("touchesBegan") 44 | } 45 | 46 | @objc func addDish() { 47 | let dishs = ["回锅肉", "青椒肉丝", "麻婆豆腐", "火锅", "冷串串", "凉粉", "剁椒鱼头", "酸菜鱼", "锅盔", "天蚕土豆", "春卷"] 48 | let index = Int(arc4random()%UInt32(dishs.count)) 49 | let dish = dishs[index] 50 | dataSource.insert(dish, at: 0) 51 | if dataSource.last == "空空如也" { 52 | dataSource.removeLast() 53 | tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .none) 54 | }else { 55 | tableView.reloadData() 56 | } 57 | } 58 | 59 | @objc func deleteDish() { 60 | dataSource.removeFirst() 61 | if dataSource.count == 0 { 62 | dataSource.append("空空如也") 63 | tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .none) 64 | }else { 65 | tableView.reloadData() 66 | } 67 | } 68 | 69 | } 70 | 71 | extension ViewController: UITableViewDataSource, UITableViewDelegate { 72 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 73 | return dataSource.count 74 | } 75 | 76 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 77 | return 50 78 | } 79 | 80 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 81 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 82 | cell.selectionStyle = .none 83 | cell.textLabel?.text = dataSource[indexPath.row] 84 | return cell 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 暴走的鑫鑫 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JXBottomSheetView 2 | 3 | 项目中有类似于外卖软件的已点菜品列表,类似于下图: 4 | 5 | ![meituan.gif](https://upload-images.jianshu.io/upload_images/1085173-77587ed9d77747a8.gif?imageMogr2/auto-orient/strip) 6 | 7 | 可以看到列表的显示与隐藏,都只能通过按钮触发。不能通过手势驱动。不能设置最小可显示范围。针对以上问题,就有了这个项目。 8 | 9 | 其实以上的需求核心问题就一个,如何优雅的解决:当内容还未到最大可显示范围时,列表里的内容不能滚动;当内容显示到最大的时候,如何不断开当前滚动手势,继续滚动列表里的内容。 10 | 11 | 之前写了一个类似的底部列表滚动视图,地址:https://github.com/pujiaxin33/JXBottomSheetTableView 里面的实现方案还是挺有趣的,对外完全封装了里面的滚动控制细节,且以UITableView的子类实现。无奈越骚的操作越容易翻车。里面的应用场景比较狭窄,需求一变动就GG了。 12 | 所以重新写了这个库,使用场景更大,使用更方便,交互更友善,好了,不说了,快上车吧! 13 | 14 | # 原理 15 | 16 | 为`JXBottomSheetView`添加一个`UIPanGestureRecognizer`,成为其`delegate`,并让`shouldRecognizeSimultaneouslyWithOtherGestureRecognizer`方法返回true; 17 | 如此一来,内容承载视图与列表视图的滚动手势可以同时响应了。接着,我们需要处理好当内容承载视图未显示到最大值时,列表视图(UITableView、UICollectionView)的`contentOffset.y`会被强制设置为0,营造一种列表内容未滚动的假象; 18 | 当内容承载视图滚动到最大的时候,就放开对列表视图的滚动限制。 19 | 其他一些细节可以参看源码了解; 20 | 21 | # 特性 22 | 23 | - 支持长距离滚动,不断手势:当列表视图滚动到规定的最高点时,停止视图移动,转而滚动里面的内容; 24 | - 内容自适应:当列表的数据源发生变动时,会根据最新的`contentSize`调整布局; 25 | - 切换流畅:最大、最小的手势切换,借鉴了系统`UIScrollView`的`PagingEnabled`切换效果; 26 | 27 | # 预览 28 | 29 | - 普通短距离滚动 30 | 31 | ![](https://github.com/pujiaxin33/JXBottomSheetView/blob/master/JXBottomSheetView/Gif/NormalScroll.gif) 32 | 33 | - 长距离滚动,手势没有停掉。滚动到顶部的时候,继续滚动里面的内容 34 | 35 | ![](https://github.com/pujiaxin33/JXBottomSheetView/blob/master/JXBottomSheetView/Gif/Scroll.gif) 36 | 37 | - 内容自适应,根据`contentView`的`contentSize`自动调整布局 38 | 39 | ![](https://github.com/pujiaxin33/JXBottomSheetView/blob/master/JXBottomSheetView/Gif/Changed.gif) 40 | 41 | # 属性/方法 42 | 43 | 属性/方法 | 描述 | 44 | ----|------| 45 | **defaultMininumDisplayHeight** | 默认最小内容高度,当contentSize.height更小时,会更新mininumDisplayHeight值。 | 46 | **defaultMaxinumDisplayHeight** | 默认最大内容高度,当contentSize.height更小时,会更新maxinumDisplayHeight值。 | 47 | **displayState** | 当前展示状态,最大或最小 | 48 | **triggerDistance** | 滚动多少距离,可以触发展开和收缩状态切换。 | 49 | **triggerVelocity** | 触发状态切换的滚动速度,points/second | 50 | **contentView: UIScrollView** | 用于承载内容的视图,UITableView、UICollectionView皆可。 | 51 | **displayMax()** | 显示最大内容 | 52 | **displayMin()** | 显示最小内容 | 53 | 54 | 55 | # 使用 56 | 57 | ``` 58 | tableView = UITableView.init(frame: CGRect.zero, style: .plain) 59 | 60 | let bottomSheet = JXBottomSheetView(contentView: tableView) 61 | bottomSheet.defaultMininumDisplayHeight = 100 62 | bottomSheet.defaultMaxinumDisplayHeight = 300 63 | bottomSheet.displayState = .minDisplay 64 | bottomSheet.frame = self.view.bounds 65 | view.addSubview(bottomSheet) 66 | ``` 67 | 68 | # 安装 69 | 70 | swift版本:5.0+ 71 | 72 | ```ruby 73 | use_frameworks! 74 | target '' do 75 | pod 'JXBottomSheetView' 76 | end 77 | ``` 78 | 79 | # 注意 80 | 81 | - 内部会影响到外部的代码 82 | ```contentView.bounces = false 83 | if let tableView = contentView as? UITableView { 84 | tableView.estimatedRowHeight = 0 85 | } 86 | ``` 87 | 88 | - 数据源的增删,请使用`reloadData`,而不是`insertRows`、`deleteRows`刷新页面。因为...你试一下就知道了。 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Sources/JXBottomSheetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXBottomSheetView.swift 3 | // JXBottomSheetView 4 | // 5 | // Created by jiaxin on 2018/8/1. 6 | // Copyright © 2018年 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public enum JXBottomSheetState: Int { 12 | case maxDisplay 13 | case minDisplay 14 | } 15 | 16 | @objc public protocol JXBottomSheetViewDelegate { 17 | @objc optional func bottomSheet(bottomSheet: JXBottomSheetView, willDisplay state: JXBottomSheetState) 18 | @objc optional func bottomSheet(bottomSheet: JXBottomSheetView, didDisplayed state: JXBottomSheetState) 19 | } 20 | 21 | public class JXBottomSheetView: UIView { 22 | weak public var delegate: JXBottomSheetViewDelegate? 23 | //默认最小内容高度,当contentSize.height更小时,会更新mininumDisplayHeight值 24 | public var defaultMininumDisplayHeight: CGFloat = 100 { 25 | didSet { 26 | mininumDisplayHeight = defaultMininumDisplayHeight 27 | } 28 | } 29 | //默认最大内容高度,当contentSize.height更小时,会更新maxinumDisplayHeight值 30 | public var defaultMaxinumDisplayHeight: CGFloat = 300 { 31 | didSet { 32 | maxinumDisplayHeight = defaultMaxinumDisplayHeight 33 | } 34 | } 35 | public var displayState: JXBottomSheetState = .minDisplay 36 | //1、判断triggerVelocity,大于当前切换方向,直接切换; 37 | //2、判断triggerDistance: 38 | //2.1、当超过triggerDistance时,根据结束手势时手指的方向切换状态; 39 | //2.2、未超过triggerDistance时,恢复状态; 40 | public var triggerVelocity: CGFloat = 1000 //触发状态切换的滚动速度,points/second 41 | public var triggerDistance: CGFloat = 50 //滚动多少距离,可以触发展开和收缩状态切换 42 | fileprivate var mininumDisplayHeight: CGFloat = 100 43 | fileprivate var maxinumDisplayHeight: CGFloat = 300 44 | fileprivate var isFirstLayout = true 45 | fileprivate var minFrame: CGRect { 46 | get { 47 | return CGRect(x: 0, y: self.bounds.size.height - mininumDisplayHeight, width: self.bounds.size.width, height: maxinumDisplayHeight) 48 | } 49 | } 50 | fileprivate var maxFrame: CGRect { 51 | get { 52 | return CGRect(x: 0, y: self.bounds.size.height - maxinumDisplayHeight, width: self.bounds.size.width, height: maxinumDisplayHeight) 53 | } 54 | } 55 | fileprivate var lastContentSize: CGSize? 56 | let contentView: UIScrollView 57 | 58 | public override func willMove(toSuperview newSuperview: UIView?) { 59 | super.willMove(toSuperview: newSuperview) 60 | 61 | if newSuperview == nil { 62 | contentView.removeObserver(self, forKeyPath: "contentSize") 63 | } 64 | } 65 | 66 | public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 67 | return contentView.frame.contains(point) 68 | } 69 | 70 | public init(contentView: UIScrollView) { 71 | self.contentView = contentView 72 | super.init(frame: CGRect.zero) 73 | 74 | clipsToBounds = true 75 | backgroundColor = .clear 76 | contentView.bounces = false 77 | if let tableView = contentView as? UITableView { 78 | tableView.estimatedRowHeight = 0 79 | } 80 | addSubview(contentView) 81 | contentView.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: nil) 82 | 83 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(processPan(gesture:))) 84 | panGesture.delegate = self 85 | contentView.addGestureRecognizer(panGesture) 86 | } 87 | 88 | required public init?(coder aDecoder: NSCoder) { 89 | fatalError("init(coder:) has not been implemented") 90 | } 91 | 92 | override public func layoutSubviews() { 93 | super.layoutSubviews() 94 | 95 | if isFirstLayout { 96 | isFirstLayout = false 97 | if displayState == .minDisplay { 98 | contentView.frame = minFrame 99 | }else { 100 | contentView.frame = maxFrame 101 | } 102 | } 103 | } 104 | 105 | @objc fileprivate func processPan(gesture: UIPanGestureRecognizer) { 106 | if mininumDisplayHeight == maxinumDisplayHeight { 107 | return 108 | } 109 | switch gesture.state { 110 | case .changed: 111 | var canMoveFrame = false 112 | if displayState == .minDisplay { 113 | canMoveFrame = true 114 | }else { 115 | if contentView.frame.origin.y > maxFrame.origin.y || contentView.contentOffset.y <= 0 { 116 | canMoveFrame = true 117 | } 118 | } 119 | if canMoveFrame { 120 | let point = gesture.translation(in: contentView) 121 | var frame = contentView.frame 122 | frame.origin.y += point.y 123 | frame.origin.y = max(frame.origin.y, maxFrame.origin.y) 124 | frame.origin.y = min(frame.origin.y, minFrame.origin.y) 125 | contentView.frame = frame 126 | } 127 | gesture.setTranslation(CGPoint.zero, in: contentView) 128 | if displayState == .minDisplay { 129 | if contentView.frame.origin.y <= maxFrame.origin.y { 130 | delegate?.bottomSheet?(bottomSheet: self, willDisplay: .maxDisplay) 131 | displayState = .maxDisplay 132 | delegate?.bottomSheet?(bottomSheet: self, didDisplayed: .maxDisplay) 133 | } 134 | }else { 135 | if contentView.frame.origin.y >= minFrame.origin.y { 136 | delegate?.bottomSheet?(bottomSheet: self, willDisplay: .minDisplay) 137 | displayState = .minDisplay 138 | delegate?.bottomSheet?(bottomSheet: self, didDisplayed: .minDisplay) 139 | } 140 | } 141 | 142 | if contentView.frame.origin.y > maxFrame.origin.y || 143 | (contentView.frame.origin.y == minFrame.origin.y && contentView.frame.origin.y == maxFrame.origin.y) { 144 | //当contentView本身还未滚动到最大显示值时,内部的内容不允许滚动。mininumDisplayHeight = maxinumDisplayHeight时也不允许内部内容滚动。 145 | contentView.setContentOffset(CGPoint.zero, animated: false) 146 | } 147 | case .cancelled, .ended, .failed: 148 | let velocity = gesture.velocity(in: gesture.view) 149 | if displayState == .minDisplay { 150 | if velocity.y < -triggerVelocity { 151 | displayMax() 152 | }else if minFrame.origin.y - contentView.frame.origin.y > triggerDistance { 153 | if velocity.y <= 0 { 154 | //往上滚 155 | displayMax() 156 | }else { 157 | //往下滚 158 | displayMin() 159 | } 160 | }else { 161 | displayMin() 162 | } 163 | contentView.setContentOffset(CGPoint.zero, animated: false) 164 | }else { 165 | if velocity.y > triggerVelocity && contentView.contentOffset.y <= 0 { 166 | displayMin() 167 | contentView.setContentOffset(CGPoint.zero, animated: false) 168 | }else if contentView.frame.origin.y - maxFrame.origin.y > triggerDistance { 169 | if velocity.y < 0 { 170 | //往上滚 171 | displayMax() 172 | }else { 173 | //往下滚 174 | displayMin() 175 | } 176 | }else { 177 | displayMax() 178 | } 179 | } 180 | default: 181 | break 182 | } 183 | } 184 | 185 | public func displayMax() { 186 | if contentView.frame == maxFrame { 187 | return 188 | } 189 | delegate?.bottomSheet?(bottomSheet: self, willDisplay: JXBottomSheetState.maxDisplay) 190 | UIView.animate(withDuration: 0.25, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { 191 | self.contentView.frame = self.maxFrame 192 | }) { (finished) in 193 | self.displayState = .maxDisplay 194 | self.delegate?.bottomSheet?(bottomSheet: self, didDisplayed: JXBottomSheetState.maxDisplay) 195 | } 196 | } 197 | 198 | public func displayMin() { 199 | if contentView.frame == minFrame { 200 | return 201 | } 202 | delegate?.bottomSheet?(bottomSheet: self, willDisplay: JXBottomSheetState.minDisplay) 203 | UIView.animate(withDuration: 0.25, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { 204 | self.contentView.frame = self.minFrame 205 | }) { (finished) in 206 | self.displayState = .minDisplay 207 | self.delegate?.bottomSheet?(bottomSheet: self, didDisplayed: JXBottomSheetState.minDisplay) 208 | } 209 | } 210 | 211 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 212 | if keyPath == "contentSize" { 213 | guard let newContentSize = change?[NSKeyValueChangeKey.newKey] as? CGSize else { 214 | return 215 | } 216 | guard newContentSize != lastContentSize else { 217 | return 218 | } 219 | mininumDisplayHeight = min(defaultMininumDisplayHeight, contentView.contentSize.height) 220 | maxinumDisplayHeight = min(defaultMaxinumDisplayHeight, contentView.contentSize.height) 221 | if displayState == .maxDisplay { 222 | contentView.frame = maxFrame 223 | }else { 224 | contentView.frame = minFrame 225 | } 226 | lastContentSize = newContentSize 227 | } 228 | } 229 | 230 | } 231 | 232 | extension JXBottomSheetView: UIGestureRecognizerDelegate { 233 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 234 | return true 235 | } 236 | } 237 | 238 | 239 | 240 | 241 | --------------------------------------------------------------------------------