├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── JJCarouselView-Demo ├── JJCarouselView-Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── zhengguijie.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── zhengguijie.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── JJCarouselView-Demo.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── JJCarouselView-Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Images │ │ ├── a-0.jpeg │ │ ├── a-1.jpeg │ │ ├── a-2.jpeg │ │ ├── a-3.jpeg │ │ ├── a-4.jpeg │ │ └── a-5.jpeg │ ├── Info.plist │ ├── TabBarViewController.swift │ ├── ViewController.swift │ ├── WebCarouselModel.swift │ └── WebCarouselView.swift └── Podfile ├── JJCarouselView.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── CellContainer.swift ├── Config.swift ├── Direction.swift ├── Event.swift ├── JJCarousel+namespace.swift ├── JJCarouselContainerView.swift ├── JJCarouselView.swift ├── JJCarouselViewPageable.swift ├── Providers │ ├── FullCarouselContainerView.swift │ ├── JJCarouselDotPageView.swift │ ├── JJCarouselHiddenPageView.swift │ └── JJCarouselNumberPageView.swift ├── Style.swift └── UIColor+Extension.swift └── Tests └── JJCarouselViewTests └── JJCarouselViewTests.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 | .DS_Store 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | Pods/ 46 | Pods 47 | Podfile.lock 48 | /Podfile.lock 49 | 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots/**/*.png 71 | fastlane/test_output 72 | 73 | # Code Injection 74 | # 75 | # After new code Injection tools there's a generated folder /iOSInjectionProject 76 | # https://github.com/johnno1962/injectionforxcode 77 | 78 | /.swiftpm 79 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5E03445D2800037900CB9C82 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E03445C2800037900CB9C82 /* AppDelegate.swift */; }; 11 | 5E0344612800037900CB9C82 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0344602800037900CB9C82 /* ViewController.swift */; }; 12 | 5E0344662800037A00CB9C82 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E0344652800037A00CB9C82 /* Assets.xcassets */; }; 13 | 5E0344692800037A00CB9C82 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5E0344672800037A00CB9C82 /* LaunchScreen.storyboard */; }; 14 | 5E0344722800042E00CB9C82 /* JJCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0344712800042E00CB9C82 /* JJCarouselView.swift */; }; 15 | 5E0344762800047B00CB9C82 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0344752800047B00CB9C82 /* Config.swift */; }; 16 | 5E0344782800049D00CB9C82 /* CellContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0344772800049D00CB9C82 /* CellContainer.swift */; }; 17 | 5E03447A280004BC00CB9C82 /* JJCarouselViewPageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E034479280004BB00CB9C82 /* JJCarouselViewPageable.swift */; }; 18 | 5E03448428000EEA00CB9C82 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 5E03448128000EEA00CB9C82 /* LICENSE */; }; 19 | 5E03448628000EEA00CB9C82 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 5E03448328000EEA00CB9C82 /* README.md */; }; 20 | 5E03448E2800109700CB9C82 /* a-5.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5E0344882800109700CB9C82 /* a-5.jpeg */; }; 21 | 5E03448F2800109700CB9C82 /* a-4.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5E0344892800109700CB9C82 /* a-4.jpeg */; }; 22 | 5E0344902800109700CB9C82 /* a-3.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5E03448A2800109700CB9C82 /* a-3.jpeg */; }; 23 | 5E0344912800109700CB9C82 /* a-2.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5E03448B2800109700CB9C82 /* a-2.jpeg */; }; 24 | 5E0344922800109700CB9C82 /* a-1.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5E03448C2800109700CB9C82 /* a-1.jpeg */; }; 25 | 5E0344932800109700CB9C82 /* a-0.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5E03448D2800109700CB9C82 /* a-0.jpeg */; }; 26 | 5E03449528001C3800CB9C82 /* WebCarouselModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E03449428001C3800CB9C82 /* WebCarouselModel.swift */; }; 27 | 5E03449728001C6800CB9C82 /* WebCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E03449628001C6800CB9C82 /* WebCarouselView.swift */; }; 28 | 5E2F818528041939008A511C /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2F818428041939008A511C /* Event.swift */; }; 29 | 5E34ACC628114BE3003EB1E4 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E34ACC528114BE3003EB1E4 /* UIColor+Extension.swift */; }; 30 | 5E3B8972287D190700048C25 /* JJCarousel+namespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3B8971287D190700048C25 /* JJCarousel+namespace.swift */; }; 31 | 5E3B8974287D198700048C25 /* Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3B8973287D198700048C25 /* Direction.swift */; }; 32 | 5E3B897A287D50BF00048C25 /* JJCarouselHiddenPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3B8976287D50BF00048C25 /* JJCarouselHiddenPageView.swift */; }; 33 | 5E3B897B287D50BF00048C25 /* JJCarouselDotPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3B8977287D50BF00048C25 /* JJCarouselDotPageView.swift */; }; 34 | 5E3B897C287D50BF00048C25 /* FullCarouselContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3B8978287D50BF00048C25 /* FullCarouselContainerView.swift */; }; 35 | 5E3B897D287D50BF00048C25 /* JJCarouselNumberPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3B8979287D50BF00048C25 /* JJCarouselNumberPageView.swift */; }; 36 | 5E874BE12806A6710046F4DC /* JJCarouselContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E874BE02806A6710046F4DC /* JJCarouselContainerView.swift */; }; 37 | 5E874BE32806AEEB0046F4DC /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E874BE22806AEEB0046F4DC /* Style.swift */; }; 38 | 5EF487CC280E40180093B98E /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF487CB280E40180093B98E /* TabBarViewController.swift */; }; 39 | C233C6BCA5368CE57955AA1C /* Pods_JJCarouselView_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1ACA3563D2902F00B45369A /* Pods_JJCarouselView_Demo.framework */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 359CE58ED333B2505B5CDAA4 /* Pods-JJCarouselView-Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JJCarouselView-Demo.release.xcconfig"; path = "Target Support Files/Pods-JJCarouselView-Demo/Pods-JJCarouselView-Demo.release.xcconfig"; sourceTree = ""; }; 44 | 5E0344592800037900CB9C82 /* JJCarouselView-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JJCarouselView-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 5E03445C2800037900CB9C82 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 46 | 5E0344602800037900CB9C82 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 47 | 5E0344652800037A00CB9C82 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 5E0344682800037A00CB9C82 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 5E03446A2800037A00CB9C82 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 5E0344712800042E00CB9C82 /* JJCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JJCarouselView.swift; sourceTree = ""; }; 51 | 5E0344752800047B00CB9C82 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 52 | 5E0344772800049D00CB9C82 /* CellContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellContainer.swift; sourceTree = ""; }; 53 | 5E034479280004BB00CB9C82 /* JJCarouselViewPageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JJCarouselViewPageable.swift; sourceTree = ""; }; 54 | 5E03448128000EEA00CB9C82 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 55 | 5E03448328000EEA00CB9C82 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 56 | 5E0344882800109700CB9C82 /* a-5.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "a-5.jpeg"; sourceTree = ""; }; 57 | 5E0344892800109700CB9C82 /* a-4.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "a-4.jpeg"; sourceTree = ""; }; 58 | 5E03448A2800109700CB9C82 /* a-3.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "a-3.jpeg"; sourceTree = ""; }; 59 | 5E03448B2800109700CB9C82 /* a-2.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "a-2.jpeg"; sourceTree = ""; }; 60 | 5E03448C2800109700CB9C82 /* a-1.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "a-1.jpeg"; sourceTree = ""; }; 61 | 5E03448D2800109700CB9C82 /* a-0.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "a-0.jpeg"; sourceTree = ""; }; 62 | 5E03449428001C3800CB9C82 /* WebCarouselModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebCarouselModel.swift; sourceTree = ""; }; 63 | 5E03449628001C6800CB9C82 /* WebCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebCarouselView.swift; sourceTree = ""; }; 64 | 5E2F818428041939008A511C /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 65 | 5E34ACC528114BE3003EB1E4 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; 66 | 5E3B8971287D190700048C25 /* JJCarousel+namespace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JJCarousel+namespace.swift"; sourceTree = ""; }; 67 | 5E3B8973287D198700048C25 /* Direction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Direction.swift; sourceTree = ""; }; 68 | 5E3B8976287D50BF00048C25 /* JJCarouselHiddenPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JJCarouselHiddenPageView.swift; sourceTree = ""; }; 69 | 5E3B8977287D50BF00048C25 /* JJCarouselDotPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JJCarouselDotPageView.swift; sourceTree = ""; }; 70 | 5E3B8978287D50BF00048C25 /* FullCarouselContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullCarouselContainerView.swift; sourceTree = ""; }; 71 | 5E3B8979287D50BF00048C25 /* JJCarouselNumberPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JJCarouselNumberPageView.swift; sourceTree = ""; }; 72 | 5E874BE02806A6710046F4DC /* JJCarouselContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JJCarouselContainerView.swift; sourceTree = ""; }; 73 | 5E874BE22806AEEB0046F4DC /* Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; 74 | 5EF487CB280E40180093B98E /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; 75 | C1ACA3563D2902F00B45369A /* Pods_JJCarouselView_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JJCarouselView_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | E700E886F9A495C3B7BB4D33 /* Pods-JJCarouselView-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JJCarouselView-Demo.debug.xcconfig"; path = "Target Support Files/Pods-JJCarouselView-Demo/Pods-JJCarouselView-Demo.debug.xcconfig"; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | 5E0344562800037900CB9C82 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | C233C6BCA5368CE57955AA1C /* Pods_JJCarouselView_Demo.framework in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 5E0344502800037900CB9C82 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 5E03448128000EEA00CB9C82 /* LICENSE */, 95 | 5E03448328000EEA00CB9C82 /* README.md */, 96 | 5E0344702800040800CB9C82 /* Sources */, 97 | 5E03445B2800037900CB9C82 /* JJCarouselView-Demo */, 98 | 5E03445A2800037900CB9C82 /* Products */, 99 | C281EAAFB48684A8CF0DBB15 /* Pods */, 100 | E6432283997209AA9D0C2CBB /* Frameworks */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 5E03445A2800037900CB9C82 /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 5E0344592800037900CB9C82 /* JJCarouselView-Demo.app */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | 5E03445B2800037900CB9C82 /* JJCarouselView-Demo */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 5E0344872800109700CB9C82 /* Images */, 116 | 5E03445C2800037900CB9C82 /* AppDelegate.swift */, 117 | 5EF487CB280E40180093B98E /* TabBarViewController.swift */, 118 | 5E0344602800037900CB9C82 /* ViewController.swift */, 119 | 5E03449628001C6800CB9C82 /* WebCarouselView.swift */, 120 | 5E03449428001C3800CB9C82 /* WebCarouselModel.swift */, 121 | 5E0344652800037A00CB9C82 /* Assets.xcassets */, 122 | 5E0344672800037A00CB9C82 /* LaunchScreen.storyboard */, 123 | 5E03446A2800037A00CB9C82 /* Info.plist */, 124 | ); 125 | path = "JJCarouselView-Demo"; 126 | sourceTree = ""; 127 | }; 128 | 5E0344702800040800CB9C82 /* Sources */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 5E3B8971287D190700048C25 /* JJCarousel+namespace.swift */, 132 | 5E0344712800042E00CB9C82 /* JJCarouselView.swift */, 133 | 5E874BE22806AEEB0046F4DC /* Style.swift */, 134 | 5E3B8973287D198700048C25 /* Direction.swift */, 135 | 5E0344752800047B00CB9C82 /* Config.swift */, 136 | 5E2F818428041939008A511C /* Event.swift */, 137 | 5E874BE02806A6710046F4DC /* JJCarouselContainerView.swift */, 138 | 5E034479280004BB00CB9C82 /* JJCarouselViewPageable.swift */, 139 | 5E0344772800049D00CB9C82 /* CellContainer.swift */, 140 | 5E34ACC528114BE3003EB1E4 /* UIColor+Extension.swift */, 141 | 5E3B8975287D50BF00048C25 /* Providers */, 142 | ); 143 | name = Sources; 144 | path = ../Sources; 145 | sourceTree = ""; 146 | }; 147 | 5E0344872800109700CB9C82 /* Images */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 5E0344882800109700CB9C82 /* a-5.jpeg */, 151 | 5E0344892800109700CB9C82 /* a-4.jpeg */, 152 | 5E03448A2800109700CB9C82 /* a-3.jpeg */, 153 | 5E03448B2800109700CB9C82 /* a-2.jpeg */, 154 | 5E03448C2800109700CB9C82 /* a-1.jpeg */, 155 | 5E03448D2800109700CB9C82 /* a-0.jpeg */, 156 | ); 157 | path = Images; 158 | sourceTree = ""; 159 | }; 160 | 5E3B8975287D50BF00048C25 /* Providers */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 5E3B8978287D50BF00048C25 /* FullCarouselContainerView.swift */, 164 | 5E3B8976287D50BF00048C25 /* JJCarouselHiddenPageView.swift */, 165 | 5E3B8977287D50BF00048C25 /* JJCarouselDotPageView.swift */, 166 | 5E3B8979287D50BF00048C25 /* JJCarouselNumberPageView.swift */, 167 | ); 168 | path = Providers; 169 | sourceTree = ""; 170 | }; 171 | C281EAAFB48684A8CF0DBB15 /* Pods */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | E700E886F9A495C3B7BB4D33 /* Pods-JJCarouselView-Demo.debug.xcconfig */, 175 | 359CE58ED333B2505B5CDAA4 /* Pods-JJCarouselView-Demo.release.xcconfig */, 176 | ); 177 | path = Pods; 178 | sourceTree = ""; 179 | }; 180 | E6432283997209AA9D0C2CBB /* Frameworks */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | C1ACA3563D2902F00B45369A /* Pods_JJCarouselView_Demo.framework */, 184 | ); 185 | name = Frameworks; 186 | sourceTree = ""; 187 | }; 188 | /* End PBXGroup section */ 189 | 190 | /* Begin PBXNativeTarget section */ 191 | 5E0344582800037900CB9C82 /* JJCarouselView-Demo */ = { 192 | isa = PBXNativeTarget; 193 | buildConfigurationList = 5E03446D2800037A00CB9C82 /* Build configuration list for PBXNativeTarget "JJCarouselView-Demo" */; 194 | buildPhases = ( 195 | 722B92B046888C91BAA37381 /* [CP] Check Pods Manifest.lock */, 196 | 5E0344552800037900CB9C82 /* Sources */, 197 | 5E0344562800037900CB9C82 /* Frameworks */, 198 | 5E0344572800037900CB9C82 /* Resources */, 199 | 0921E51A191B0F557C99B6E7 /* [CP] Embed Pods Frameworks */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | ); 205 | name = "JJCarouselView-Demo"; 206 | productName = "JJCarouselView-Demo"; 207 | productReference = 5E0344592800037900CB9C82 /* JJCarouselView-Demo.app */; 208 | productType = "com.apple.product-type.application"; 209 | }; 210 | /* End PBXNativeTarget section */ 211 | 212 | /* Begin PBXProject section */ 213 | 5E0344512800037900CB9C82 /* Project object */ = { 214 | isa = PBXProject; 215 | attributes = { 216 | BuildIndependentTargetsInParallel = 1; 217 | LastSwiftUpdateCheck = 1330; 218 | LastUpgradeCheck = 1330; 219 | TargetAttributes = { 220 | 5E0344582800037900CB9C82 = { 221 | CreatedOnToolsVersion = 13.3; 222 | }; 223 | }; 224 | }; 225 | buildConfigurationList = 5E0344542800037900CB9C82 /* Build configuration list for PBXProject "JJCarouselView-Demo" */; 226 | compatibilityVersion = "Xcode 13.0"; 227 | developmentRegion = en; 228 | hasScannedForEncodings = 0; 229 | knownRegions = ( 230 | en, 231 | Base, 232 | ); 233 | mainGroup = 5E0344502800037900CB9C82; 234 | productRefGroup = 5E03445A2800037900CB9C82 /* Products */; 235 | projectDirPath = ""; 236 | projectRoot = ""; 237 | targets = ( 238 | 5E0344582800037900CB9C82 /* JJCarouselView-Demo */, 239 | ); 240 | }; 241 | /* End PBXProject section */ 242 | 243 | /* Begin PBXResourcesBuildPhase section */ 244 | 5E0344572800037900CB9C82 /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 5E0344932800109700CB9C82 /* a-0.jpeg in Resources */, 249 | 5E03448E2800109700CB9C82 /* a-5.jpeg in Resources */, 250 | 5E03448428000EEA00CB9C82 /* LICENSE in Resources */, 251 | 5E0344902800109700CB9C82 /* a-3.jpeg in Resources */, 252 | 5E03448F2800109700CB9C82 /* a-4.jpeg in Resources */, 253 | 5E0344912800109700CB9C82 /* a-2.jpeg in Resources */, 254 | 5E0344692800037A00CB9C82 /* LaunchScreen.storyboard in Resources */, 255 | 5E03448628000EEA00CB9C82 /* README.md in Resources */, 256 | 5E0344662800037A00CB9C82 /* Assets.xcassets in Resources */, 257 | 5E0344922800109700CB9C82 /* a-1.jpeg in Resources */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | /* End PBXResourcesBuildPhase section */ 262 | 263 | /* Begin PBXShellScriptBuildPhase section */ 264 | 0921E51A191B0F557C99B6E7 /* [CP] Embed Pods Frameworks */ = { 265 | isa = PBXShellScriptBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | ); 269 | inputFileListPaths = ( 270 | "${PODS_ROOT}/Target Support Files/Pods-JJCarouselView-Demo/Pods-JJCarouselView-Demo-frameworks-${CONFIGURATION}-input-files.xcfilelist", 271 | ); 272 | name = "[CP] Embed Pods Frameworks"; 273 | outputFileListPaths = ( 274 | "${PODS_ROOT}/Target Support Files/Pods-JJCarouselView-Demo/Pods-JJCarouselView-Demo-frameworks-${CONFIGURATION}-output-files.xcfilelist", 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | shellPath = /bin/sh; 278 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JJCarouselView-Demo/Pods-JJCarouselView-Demo-frameworks.sh\"\n"; 279 | showEnvVarsInLog = 0; 280 | }; 281 | 722B92B046888C91BAA37381 /* [CP] Check Pods Manifest.lock */ = { 282 | isa = PBXShellScriptBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | ); 286 | inputFileListPaths = ( 287 | ); 288 | inputPaths = ( 289 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 290 | "${PODS_ROOT}/Manifest.lock", 291 | ); 292 | name = "[CP] Check Pods Manifest.lock"; 293 | outputFileListPaths = ( 294 | ); 295 | outputPaths = ( 296 | "$(DERIVED_FILE_DIR)/Pods-JJCarouselView-Demo-checkManifestLockResult.txt", 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | shellPath = /bin/sh; 300 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 301 | showEnvVarsInLog = 0; 302 | }; 303 | /* End PBXShellScriptBuildPhase section */ 304 | 305 | /* Begin PBXSourcesBuildPhase section */ 306 | 5E0344552800037900CB9C82 /* Sources */ = { 307 | isa = PBXSourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | 5E0344722800042E00CB9C82 /* JJCarouselView.swift in Sources */, 311 | 5E34ACC628114BE3003EB1E4 /* UIColor+Extension.swift in Sources */, 312 | 5E0344612800037900CB9C82 /* ViewController.swift in Sources */, 313 | 5E3B897B287D50BF00048C25 /* JJCarouselDotPageView.swift in Sources */, 314 | 5E3B8974287D198700048C25 /* Direction.swift in Sources */, 315 | 5E03445D2800037900CB9C82 /* AppDelegate.swift in Sources */, 316 | 5EF487CC280E40180093B98E /* TabBarViewController.swift in Sources */, 317 | 5E2F818528041939008A511C /* Event.swift in Sources */, 318 | 5E874BE12806A6710046F4DC /* JJCarouselContainerView.swift in Sources */, 319 | 5E0344782800049D00CB9C82 /* CellContainer.swift in Sources */, 320 | 5E03447A280004BC00CB9C82 /* JJCarouselViewPageable.swift in Sources */, 321 | 5E3B897D287D50BF00048C25 /* JJCarouselNumberPageView.swift in Sources */, 322 | 5E03449528001C3800CB9C82 /* WebCarouselModel.swift in Sources */, 323 | 5E3B897A287D50BF00048C25 /* JJCarouselHiddenPageView.swift in Sources */, 324 | 5E3B897C287D50BF00048C25 /* FullCarouselContainerView.swift in Sources */, 325 | 5E874BE32806AEEB0046F4DC /* Style.swift in Sources */, 326 | 5E0344762800047B00CB9C82 /* Config.swift in Sources */, 327 | 5E03449728001C6800CB9C82 /* WebCarouselView.swift in Sources */, 328 | 5E3B8972287D190700048C25 /* JJCarousel+namespace.swift in Sources */, 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | /* End PBXSourcesBuildPhase section */ 333 | 334 | /* Begin PBXVariantGroup section */ 335 | 5E0344672800037A00CB9C82 /* LaunchScreen.storyboard */ = { 336 | isa = PBXVariantGroup; 337 | children = ( 338 | 5E0344682800037A00CB9C82 /* Base */, 339 | ); 340 | name = LaunchScreen.storyboard; 341 | sourceTree = ""; 342 | }; 343 | /* End PBXVariantGroup section */ 344 | 345 | /* Begin XCBuildConfiguration section */ 346 | 5E03446B2800037A00CB9C82 /* Debug */ = { 347 | isa = XCBuildConfiguration; 348 | buildSettings = { 349 | ALWAYS_SEARCH_USER_PATHS = NO; 350 | CLANG_ANALYZER_NONNULL = YES; 351 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 352 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_ENABLE_OBJC_WEAK = YES; 356 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 357 | CLANG_WARN_BOOL_CONVERSION = YES; 358 | CLANG_WARN_COMMA = YES; 359 | CLANG_WARN_CONSTANT_CONVERSION = YES; 360 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 361 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 362 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 363 | CLANG_WARN_EMPTY_BODY = YES; 364 | CLANG_WARN_ENUM_CONVERSION = YES; 365 | CLANG_WARN_INFINITE_RECURSION = YES; 366 | CLANG_WARN_INT_CONVERSION = YES; 367 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 369 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 371 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 372 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 373 | CLANG_WARN_STRICT_PROTOTYPES = YES; 374 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 375 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 376 | CLANG_WARN_UNREACHABLE_CODE = YES; 377 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 378 | COPY_PHASE_STRIP = NO; 379 | DEBUG_INFORMATION_FORMAT = dwarf; 380 | ENABLE_STRICT_OBJC_MSGSEND = YES; 381 | ENABLE_TESTABILITY = YES; 382 | GCC_C_LANGUAGE_STANDARD = gnu11; 383 | GCC_DYNAMIC_NO_PIC = NO; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_OPTIMIZATION_LEVEL = 0; 386 | GCC_PREPROCESSOR_DEFINITIONS = ( 387 | "DEBUG=1", 388 | "$(inherited)", 389 | ); 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 397 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 398 | MTL_FAST_MATH = YES; 399 | ONLY_ACTIVE_ARCH = YES; 400 | SDKROOT = iphoneos; 401 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 403 | }; 404 | name = Debug; 405 | }; 406 | 5E03446C2800037A00CB9C82 /* Release */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_SEARCH_USER_PATHS = NO; 410 | CLANG_ANALYZER_NONNULL = YES; 411 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 412 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 413 | CLANG_ENABLE_MODULES = YES; 414 | CLANG_ENABLE_OBJC_ARC = YES; 415 | CLANG_ENABLE_OBJC_WEAK = YES; 416 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 417 | CLANG_WARN_BOOL_CONVERSION = YES; 418 | CLANG_WARN_COMMA = YES; 419 | CLANG_WARN_CONSTANT_CONVERSION = YES; 420 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 421 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 422 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 423 | CLANG_WARN_EMPTY_BODY = YES; 424 | CLANG_WARN_ENUM_CONVERSION = YES; 425 | CLANG_WARN_INFINITE_RECURSION = YES; 426 | CLANG_WARN_INT_CONVERSION = YES; 427 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 428 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 429 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 430 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 431 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 432 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 433 | CLANG_WARN_STRICT_PROTOTYPES = YES; 434 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 435 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 436 | CLANG_WARN_UNREACHABLE_CODE = YES; 437 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 438 | COPY_PHASE_STRIP = NO; 439 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 440 | ENABLE_NS_ASSERTIONS = NO; 441 | ENABLE_STRICT_OBJC_MSGSEND = YES; 442 | GCC_C_LANGUAGE_STANDARD = gnu11; 443 | GCC_NO_COMMON_BLOCKS = YES; 444 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 445 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 446 | GCC_WARN_UNDECLARED_SELECTOR = YES; 447 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 448 | GCC_WARN_UNUSED_FUNCTION = YES; 449 | GCC_WARN_UNUSED_VARIABLE = YES; 450 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 451 | MTL_ENABLE_DEBUG_INFO = NO; 452 | MTL_FAST_MATH = YES; 453 | SDKROOT = iphoneos; 454 | SWIFT_COMPILATION_MODE = wholemodule; 455 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 456 | VALIDATE_PRODUCT = YES; 457 | }; 458 | name = Release; 459 | }; 460 | 5E03446E2800037A00CB9C82 /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | baseConfigurationReference = E700E886F9A495C3B7BB4D33 /* Pods-JJCarouselView-Demo.debug.xcconfig */; 463 | buildSettings = { 464 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 465 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 466 | CODE_SIGN_STYLE = Automatic; 467 | CURRENT_PROJECT_VERSION = 1; 468 | DEVELOPMENT_TEAM = 4288334GUU; 469 | GENERATE_INFOPLIST_FILE = YES; 470 | INFOPLIST_FILE = "JJCarouselView-Demo/Info.plist"; 471 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 472 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 473 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 474 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 475 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 476 | LD_RUNPATH_SEARCH_PATHS = ( 477 | "$(inherited)", 478 | "@executable_path/Frameworks", 479 | ); 480 | MARKETING_VERSION = 1.0; 481 | PRODUCT_BUNDLE_IDENTIFIER = "com.JJCarouselView-Demo.zgj.JJCarouselView-Demo"; 482 | PRODUCT_NAME = "$(TARGET_NAME)"; 483 | SWIFT_EMIT_LOC_STRINGS = YES; 484 | SWIFT_VERSION = 5.0; 485 | TARGETED_DEVICE_FAMILY = "1,2"; 486 | }; 487 | name = Debug; 488 | }; 489 | 5E03446F2800037A00CB9C82 /* Release */ = { 490 | isa = XCBuildConfiguration; 491 | baseConfigurationReference = 359CE58ED333B2505B5CDAA4 /* Pods-JJCarouselView-Demo.release.xcconfig */; 492 | buildSettings = { 493 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 494 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 495 | CODE_SIGN_STYLE = Automatic; 496 | CURRENT_PROJECT_VERSION = 1; 497 | DEVELOPMENT_TEAM = 4288334GUU; 498 | GENERATE_INFOPLIST_FILE = YES; 499 | INFOPLIST_FILE = "JJCarouselView-Demo/Info.plist"; 500 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 501 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 502 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 503 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 504 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 505 | LD_RUNPATH_SEARCH_PATHS = ( 506 | "$(inherited)", 507 | "@executable_path/Frameworks", 508 | ); 509 | MARKETING_VERSION = 1.0; 510 | PRODUCT_BUNDLE_IDENTIFIER = "com.JJCarouselView-Demo.zgj.JJCarouselView-Demo"; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SWIFT_EMIT_LOC_STRINGS = YES; 513 | SWIFT_VERSION = 5.0; 514 | TARGETED_DEVICE_FAMILY = "1,2"; 515 | }; 516 | name = Release; 517 | }; 518 | /* End XCBuildConfiguration section */ 519 | 520 | /* Begin XCConfigurationList section */ 521 | 5E0344542800037900CB9C82 /* Build configuration list for PBXProject "JJCarouselView-Demo" */ = { 522 | isa = XCConfigurationList; 523 | buildConfigurations = ( 524 | 5E03446B2800037A00CB9C82 /* Debug */, 525 | 5E03446C2800037A00CB9C82 /* Release */, 526 | ); 527 | defaultConfigurationIsVisible = 0; 528 | defaultConfigurationName = Release; 529 | }; 530 | 5E03446D2800037A00CB9C82 /* Build configuration list for PBXNativeTarget "JJCarouselView-Demo" */ = { 531 | isa = XCConfigurationList; 532 | buildConfigurations = ( 533 | 5E03446E2800037A00CB9C82 /* Debug */, 534 | 5E03446F2800037A00CB9C82 /* Release */, 535 | ); 536 | defaultConfigurationIsVisible = 0; 537 | defaultConfigurationName = Release; 538 | }; 539 | /* End XCConfigurationList section */ 540 | }; 541 | rootObject = 5E0344512800037900CB9C82 /* Project object */; 542 | } 543 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcodeproj/project.xcworkspace/xcuserdata/zhengguijie.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo.xcodeproj/project.xcworkspace/xcuserdata/zhengguijie.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcodeproj/xcuserdata/zhengguijie.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JJCarouselView-Demo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | var window: UIWindow? 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | window = UIWindow(frame: UIScreen.main.bounds) 16 | if #available(iOS 13.0, *) { 17 | window?.backgroundColor = .systemBackground 18 | } else { 19 | window?.backgroundColor = .black 20 | } 21 | window?.rootViewController = TabBarViewController() 22 | window?.makeKeyAndVisible() 23 | return true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Images/a-0.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo/Images/a-0.jpeg -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Images/a-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo/Images/a-1.jpeg -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Images/a-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo/Images/a-2.jpeg -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Images/a-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo/Images/a-3.jpeg -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Images/a-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo/Images/a-4.jpeg -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Images/a-5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zgjff/JJCarouselView/43e41437f17c3a315e6a4d84dd856e2ffef2c28d/JJCarouselView-Demo/JJCarouselView-Demo/Images/a-5.jpeg -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/TabBarViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarViewController.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/4/19. 6 | // 7 | 8 | import UIKit 9 | 10 | class TabBarViewController: UITabBarController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | tabBar.tintColor = .red 15 | let vc1 = UINavigationController(rootViewController: ViewController()) 16 | vc1.title = "轮播" 17 | 18 | let vc2 = UIViewController() 19 | vc2.title = "空白" 20 | viewControllers = [vc1, vc2] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | import SDWebImage 10 | import SafariServices 11 | import Combine 12 | 13 | class ViewController: UIViewController { 14 | 15 | private lazy var scrollView = UIScrollView() 16 | private lazy var subviewsMaxY: CGFloat = 0 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | setup() 20 | addLocalImageCarouselView1() 21 | addLocalImageCarouselView2() 22 | addWebImageCarouselView() 23 | addCustomCarouselView() 24 | scrollView.contentSize = CGSize(width: view.bounds.width, height: subviewsMaxY + 20) 25 | } 26 | } 27 | 28 | private extension ViewController { 29 | func setup() { 30 | navigationItem.title = "可以切换tabbar来观察轮播图是否暂停" 31 | scrollView.frame = view.bounds 32 | view.addSubview(scrollView) 33 | } 34 | 35 | func addLocalImageCarouselView1() { 36 | let carouselView: JJCarouselView = JJCarouselView(frame: CGRect(x: 50, y: 0, width: view.bounds.width - 100, height: 200), initialize: nil) 37 | carouselView.config.display = { cell, object in 38 | cell.clipsToBounds = true 39 | cell.contentMode = .scaleAspectFill 40 | cell.image = object 41 | } 42 | carouselView.config.manualSlidingEnable = false 43 | carouselView.config.direction = .rtl 44 | carouselView.backgroundColor = .random() 45 | carouselView.event.onTap = { _, obj, idx in 46 | print(obj, idx) 47 | } 48 | carouselView.event.willMove = { idx in 49 | print("willMove----", idx) 50 | } 51 | carouselView.event.didMove = { idx in 52 | print("didMove----", idx) 53 | } 54 | carouselView.event.onScroll = { fromIndex, toIndex, progress in 55 | // print("onScroll----", fromIndex, toIndex, progress) 56 | } 57 | subviewsMaxY = carouselView.frame.maxY 58 | scrollView.addSubview(carouselView) 59 | carouselView.datas = (0..<6).map { UIImage(named: "a-\($0).jpeg")! } 60 | } 61 | 62 | func addLocalImageCarouselView2() { 63 | let carouselView = JJLocalImageCarouselView(frame: CGRect(x: 50, y: subviewsMaxY + 30, width: view.bounds.width - 100, height: 200)) { 64 | return UIImageView() 65 | } 66 | carouselView.config.display = { cell, object in 67 | cell.contentMode = .scaleAspectFit 68 | cell.image = object 69 | } 70 | carouselView.config.direction = .ttb 71 | carouselView.config.pageViewFrame = { pageView, _, carouselViewSize, totalDataCount in 72 | let pageSize = pageView.size(forNumberOfPages: totalDataCount) 73 | return CGRect(x: carouselViewSize.width - pageSize.width - 12, y: carouselViewSize.height - pageSize.height - 10, width: pageSize.width, height: pageSize.height) 74 | } 75 | carouselView.pageView = JJCarouselNumberPageView() 76 | subviewsMaxY = carouselView.frame.maxY 77 | scrollView.addSubview(carouselView) 78 | carouselView.datas = (0..<3).map { UIImage(named: "a-\($0).jpeg")! } 79 | } 80 | 81 | func addWebImageCarouselView() { 82 | let carouselView = JJWebImageCarouselView(frame: CGRect(x: 50, y: subviewsMaxY + 30, width: view.bounds.width - 100, height: 200)) 83 | carouselView.config.display = { cell, object in 84 | cell.clipsToBounds = true 85 | cell.contentMode = .scaleAspectFill 86 | cell.sd_setImage(with: object) 87 | } 88 | carouselView.pageView = JJCarouselNumberPageView() 89 | carouselView.config.pageViewFrame = { _, _, carouselViewSize, _ in 90 | return CGRect(x: carouselViewSize.width - 55, y: carouselViewSize.height - 30, width: 45, height: 20) 91 | } 92 | subviewsMaxY = carouselView.frame.maxY 93 | scrollView.addSubview(carouselView) 94 | carouselView.datas = (1..<25).compactMap { URL(string: String(format: "http://r0k.us/graphics/kodak/kodak/kodim%02d.png", $0)) } 95 | } 96 | 97 | func addCustomCarouselView() { 98 | let carouselView: JJCarouselView = JJCarouselView(frame: CGRect(x: 50, y: subviewsMaxY + 30, width: view.bounds.width - 100, height: 150)) 99 | carouselView.config.display = { cell, object in 100 | cell.titleLabel.text = object.title 101 | cell.descLabel.text = object.desc 102 | } 103 | carouselView.event.onTap = { [weak self] _, obj, _ in 104 | let sf = SFSafariViewController(url: obj.url) 105 | self?.present(sf, animated: true) 106 | } 107 | carouselView.pageView = JJCarouselNumberPageView() 108 | subviewsMaxY = carouselView.frame.maxY 109 | scrollView.addSubview(carouselView) 110 | carouselView.datas = [ 111 | WebCarouselModel(title: "这是第1个自定义轮播控件", desc: "这是第1个自定义轮播控件", url: URL(string: "https://www.baidu.com")!), 112 | WebCarouselModel(title: "这是第2个自定义轮播控件", desc: "这是第2个自定义轮播控件", url: URL(string: "https://www.zhihu.com")!), 113 | WebCarouselModel(title: "这是第3个自定义轮播控件", desc: "这是第3个自定义轮播控件", url: URL(string: "https://cn.bing.com")!), 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/WebCarouselModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebCarouselModel.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WebCarouselModel { 11 | let title: String 12 | let desc: String 13 | let url: URL 14 | } 15 | 16 | extension WebCarouselModel: Equatable { 17 | static func == (lhs: Self, rhs: Self) -> Bool { 18 | return (lhs.title == rhs.title) && (lhs.desc == rhs.desc) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/JJCarouselView-Demo/WebCarouselView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebCarouselView.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | final class WebCarouselView: UIView { 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | backgroundColor = .purple 15 | titleLabel.font = UIFont.boldSystemFont(ofSize: 20) 16 | titleLabel.textColor = .orange 17 | titleLabel.text = " " 18 | titleLabel.sizeToFit() 19 | addSubview(titleLabel) 20 | descLabel.font = UIFont.systemFont(ofSize: 15) 21 | descLabel.textColor = .gray 22 | descLabel.text = " " 23 | descLabel.sizeToFit() 24 | addSubview(descLabel) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func layoutSubviews() { 32 | super.layoutSubviews() 33 | titleLabel.frame = CGRect(x: 16, y: 30, width: bounds.width - 32, height: titleLabel.frame.height) 34 | descLabel.frame = CGRect(x: titleLabel.frame.minX, y: titleLabel.frame.maxY + 30, width: titleLabel.frame.width, height: descLabel.frame.height) 35 | } 36 | 37 | private(set) lazy var titleLabel = UILabel() 38 | private(set) lazy var descLabel = UILabel() 39 | } 40 | -------------------------------------------------------------------------------- /JJCarouselView-Demo/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | 3 | target 'JJCarouselView-Demo' do 4 | use_frameworks! 5 | pod 'SDWebImage' 6 | end 7 | -------------------------------------------------------------------------------- /JJCarouselView.podspec: -------------------------------------------------------------------------------- 1 | @version = "0.1.1" 2 | Pod::Spec.new do |spec| 3 | spec.name = "JJCarouselView" 4 | spec.version = @version 5 | spec.summary = "Swift简单好用、易于扩展的轮播图框架." 6 | spec.description = "适用于Swift的简单好用、易于扩展的轮播图框架." 7 | spec.homepage = "https://github.com/zgjff/JJCarouselView" 8 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 9 | spec.author = { "zgjff" => "zguijie1005@163.com" } 10 | spec.source = { :git => "https://github.com/zgjff/JJCarouselView.git", :tag => "#{spec.version}" } 11 | spec.source_files = 'Sources/*.swift', 'Sources/**/*.{swift}' 12 | spec.platform = :ios, "9.0" 13 | spec.swift_version = '5.0' 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zgjff 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "JJCarouselView", 8 | platforms: [.iOS(.v9)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "JJCarouselView", 13 | targets: ["JJCarouselView"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target(name: "JJCarouselView", path: "Sources") 23 | ], 24 | swiftLanguageVersions: [ 25 | .v5 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JJCarouselView 2 | ================= 3 | 4 | [![GitHub](https://img.shields.io/github/license/zgjff/JJCarouselView)](https://www.apache.org/licenses/LICENSE-2.0.html) 5 | [![swift-5.0](https://img.shields.io/badge/swift-5.0-blue)](https://www.swift.org) 6 | ![iOS-9.0](https://img.shields.io/badge/iOS-9.0-red) 7 | [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/zgjff/JJCarouselView)](https://github.com/zgjff/JJCarouselView) 8 | [![SwiftPM support](https://img.shields.io/badge/SwiftPM-support-brightgreen)](https://www.swift.org/package-manager/) 9 | [![Cocoapods](https://img.shields.io/cocoapods/v/JJCarouselView)](https://cocoapods.org/pods/JJCarouselView) 10 | 11 | 适用于Swift的简单好用、易于扩展的轮播图框架 12 | 13 | 使用方法 14 | ================= 15 | 16 | ## 一、初始化 17 | 因为本轮播图是泛型控件,所以在初始化的时候需要指定类型。 18 | ```swift 19 | let carouselView: JJCarouselView = JJCarouselView(frame: CGRect.zero) 20 | 21 | let carouselView: JJCarouselView = JJCarouselView(frame: CGRect.zero) { 22 | return UIImageView() 23 | } 24 | ``` 25 | 26 | ## 二、使用方法 27 | 28 | ### 2.1 基本设置 29 | 轮播图方向,默认从左->右 30 | ```swift 31 | cv.config.direction = .ltr 32 | ``` 33 | 34 | 是否自动轮播,默认`true`自动轮播 35 | ```swift 36 | cv.config.autoLoop = true 37 | ``` 38 | 39 | 轮播间隔,默认5s 40 | ```swift 41 | cv.config.loopTimeInterval = 5 42 | ``` 43 | 44 | 轮播图内边局 45 | ```swift 46 | cv.config.contentInset = .zero 47 | ``` 48 | 49 | 50 | ### 2.2 展示数据 51 | > 本控件没指定将对应的数据源显示到容器的方法,所以需要自己去实现。只要在初始化`JJCarouselView`之后,设置`config.display`即可。 52 | 53 | #### 2.2.1 以最基本的展示本地图片的轮播图为例: 54 | ```swift 55 | let carouselView: JJLocalImageCarouselView 56 | cv.config.display = { cell, image in 57 | cell.clipsToBounds = true 58 | cell.contentMode = .scaleAspectFill 59 | cell.image = image 60 | } 61 | ``` 62 | 63 | #### 2.2.2 展示网络图片 64 | ```swift 65 | let carouselView: JJWebImageCarouselView 66 | cv.config.display = { cell, url in 67 | ... 68 | // 使用SDWebImage 69 | cell.sd_setImage(with: url) 70 | // 使用Kingfisher 71 | cell.kf.setImage(with: url) 72 | } 73 | ``` 74 | 75 | #### 2.2.3 展示任何你想轮播的内容 76 | 可以使用任何`UIView`的子类来展示任意对象,只需设定轮播图类型的`Object`遵守`Equatable`协议即可。 77 | ```swift 78 | // 轮播Model,必须遵守Equatable协议 79 | struct WebCarouselModel { 80 | let title: String 81 | let desc: String 82 | let url: URL 83 | } 84 | 85 | extension WebCarouselModel: Equatable { 86 | static func == (lhs: Self, rhs: Self) -> Bool { 87 | return (lhs.title == rhs.title) && (lhs.desc == rhs.desc) 88 | } 89 | } 90 | ``` 91 | ```swift 92 | // 轮播控件 93 | final class WebCarouselView: UIView {} 94 | ``` 95 | ```swift 96 | let cv: JJCarouselView = JJCarouselView(frame: CGRect(x: 50, y: 0, width: 200, height: 150), initialize: nil) 97 | cv.config.display = { cell, object in 98 | cell.titleLabel.text = object.title 99 | cell.descLabel.text = object.desc 100 | } 101 | cv.datas = [ 102 | WebCarouselModel(title: "这是第1个自定义轮播控件", desc: "这是第1个自定义轮播控件", url: URL(string: "https://www.baidu.com")!), 103 | WebCarouselModel(title: "这是第2个自定义轮播控件", desc: "这是第2个自定义轮播控件", url: URL(string: "https://www.zhihu.com")!), 104 | WebCarouselModel(title: "这是第3个自定义轮播控件", desc: "这是第3个自定义轮播控件", url: URL(string: "https://cn.bing.com")!), 105 | ] 106 | ``` 107 | 108 | ### 2.3 轮播指示器 109 | 110 | #### 2.3.1 指示器控件 111 | 轮播图的`pageView`是可替换的,只需要替换成遵守`JJCarouselViewPageable`协议的类类即可。 112 | ```swift 113 | cv.pageView = JJCarouselNumberPageView() 114 | ``` 115 | 隐藏指示器,只需要将`pageView`设置成`JJCarouselHiddenPageView`。 116 | ```swift 117 | cv.pageView = JJCarouselHiddenPageView() 118 | ``` 119 | 当然你也可以自定义专属于你的指示器 120 | ```swift 121 | class YourOwnPageView: UIView, JJCarouselViewPageable { 122 | ... 123 | } 124 | ``` 125 | ```swift 126 | cv.pageView = YourOwnPageView() 127 | ``` 128 | 129 | #### 2.3.2 指示器控件`frame` 130 | 默认底部居中显示指示器,当然你也可以设定任何位置。 131 | ```swift 132 | // 根据数量来设定frame 133 | cv.config.pageViewFrame = { pageView, _, carouselViewSize, totalDataCount in 134 | let pageSize = pageView.size(forNumberOfPages: totalDataCount) 135 | return CGRect(x: carouselViewSize.width - pageSize.width - 12, y: carouselViewSize.height - pageSize.height - 10, width: pageSize.width, height: pageSize.height) 136 | } 137 | ``` 138 | ```swift 139 | // 固定大小 140 | cv.config.pageViewFrame = { _, _, carouselViewSize, _ in 141 | return CGRect(x: carouselViewSize.width - 55, y: carouselViewSize.height - 30, width: 45, height: 20) 142 | } 143 | ``` 144 | 145 | ### 2.4 事件回调 146 | 147 | 点击事件 148 | ```swift 149 | // 使用block 150 | cv.event.onTap = { view, obj, index in 151 | ... 152 | } 153 | // 使用Combine框架 154 | cv.event.onTapPublisher.sink { view, obj, idx in 155 | ... 156 | }.store(in: &cancellable) 157 | ``` 158 | 准备滑动到具体的index 159 | ```swift 160 | // 使用block 161 | cv.event.willMove = { idx in 162 | ... 163 | } 164 | // 使用Combine框架 165 | cv.event.willMovePublisher.sink(receiveValue: { idx in 166 | ... 167 | }).store(in: &cancellable) 168 | ``` 169 | 已经滑动到具体的index 170 | ```swift 171 | // 使用block 172 | cv.event.didMove = { idx in 173 | ... 174 | } 175 | // 使用Combine框架 176 | cv.event.didMovePublisher.sink(receiveValue: { idx in 177 | ... 178 | }).store(in: &cancellable) 179 | ``` 180 | 滑动回调(当前index, 目标index, 进度) 181 | ```swift 182 | // 使用block 183 | cv.event.onScroll = { fromIndex, toIndex, progress in 184 | ... 185 | } 186 | // 使用Combine框架 187 | cv.event.onScrollPublisher.sink(receiveValue: { fromIndex, toIndex, progress in 188 | ... 189 | }).store(in: &cancellable) 190 | ``` 191 | 192 | 193 | 使用需求 194 | ================= 195 | * iOS 9.0+ 196 | * Swift 5+ 197 | 198 | 安装 199 | ================= 200 | Swift Package Manager 201 | * File > Swift Packages > Add Package Dependency 202 | * Add https://github.com/zgjff/JJCarouselView.git 203 | 204 | Cocoapods 205 | ``` 206 | use_frameworks! 207 | pod 'JJCarouselView' 208 | ``` 209 | -------------------------------------------------------------------------------- /Sources/CellContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellContainer.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | extension JJCarouselView { 11 | /// 轮播图cell+index的容器类 12 | final class CellContainer { 13 | var onTap: ((Cell, Int) -> ())? 14 | init(cell: Cell, index: Int) { 15 | cell.isUserInteractionEnabled = true 16 | self.cell = cell 17 | self.index = index 18 | self.cell.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onClickCell))) 19 | } 20 | let cell: Cell 21 | var index = 0 22 | 23 | @IBAction private func onClickCell() { 24 | onTap?(cell, index) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | extension JJCarouselView { 11 | /// 轮播图配置 12 | public struct Config { 13 | /// 自动滑动方向,默认左->右 14 | public var direction = JJCarousel.Direction.ltr 15 | 16 | /// 是否自动轮播,默认`true`自动轮播 17 | public var autoLoop = true 18 | 19 | /// 能否手动滑动,默认`true`可以手动滑动 20 | public var manualSlidingEnable = true 21 | 22 | /// 轮播间隔,默认5s 23 | public var loopTimeInterval: TimeInterval = 5 24 | 25 | /// 轮播图内边局 26 | public var contentInset = UIEdgeInsets.zero 27 | 28 | /// 具体显示轮播内容的方法 29 | public var display: ((Cell, Object) -> ())? 30 | 31 | /// 计算pageView的frame, 默认底部居中显示 32 | public var pageViewFrame: ((_ pageView: JJCarouselViewPageable, _ direction: JJCarousel.Direction, _ carouselViewSize: CGSize, _ totalDataCount: Int) -> CGRect) = { pageView, _, carouselViewSize, totalDataCount in 33 | let pageSize = pageView.size(forNumberOfPages: totalDataCount) 34 | return CGRect(x: (carouselViewSize.width - pageSize.width) * 0.5, y: carouselViewSize.height - pageSize.height - 5, width: pageSize.width, height: pageSize.height) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Direction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direction.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/7/12. 6 | // 7 | 8 | import Foundation 9 | 10 | extension JJCarousel { 11 | /// 轮播图滑动方向 12 | public enum Direction { 13 | /// 左->右 14 | case ltr 15 | /// 右->左 16 | case rtl 17 | /// 上->下 18 | case ttb 19 | /// 下->上 20 | case btt 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/11. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | 11 | extension JJCarouselView { 12 | /// 轮播图事件 13 | public final class Event { 14 | /// 点击事件 15 | public var onTap: ((_ view: Cell, _ object: Object, _ index: Int) -> ())? 16 | 17 | /// 滑动回调(当前index, 目标index, 进度) 18 | public var onScroll: ((_ fronIndex: Int, _ toIndex: Int, _ progress: Float) -> ())? 19 | 20 | /// 准备滑动到目标的index 21 | public var willMove: ((_ index: Int) -> ())? 22 | 23 | /// 已经滑动到具体的index 24 | public var didMove: ((_ index: Int) -> ())? 25 | 26 | // @available(iOS 13.0, *) 27 | // internal lazy var _onTapPublisher = PassthroughSubject<(Cell, Object, Int), Never>() 28 | // 29 | // @available(iOS 13.0, *) 30 | // internal lazy var _onScrollPublisher = PassthroughSubject<(Int, Int, Float), Never>() 31 | // 32 | // @available(iOS 13.0, *) 33 | // internal lazy var _willMovePublisher = PassthroughSubject() 34 | // 35 | // @available(iOS 13.0, *) 36 | // internal lazy var _didMovePublisher = PassthroughSubject() 37 | } 38 | } 39 | 40 | internal extension JJCarouselView.Event { 41 | func onTapFunction(view: Cell, object: Object, index: Int) { 42 | onTap?(view, object, index) 43 | } 44 | 45 | func onScrollFunction(fronIndex: Int, toIndex: Int, progress: Float) { 46 | onScroll?(fronIndex, toIndex, progress) 47 | } 48 | 49 | func willMoveFunction(index: Int) { 50 | willMove?(index) 51 | } 52 | 53 | func didMoveFunction(index: Int) { 54 | didMove?(index) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/JJCarousel+namespace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJCarousel+namespace.swift 3 | // JJCarouselView-Demo 4 | // 5 | // Created by zgjff on 2022/7/12. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 命名空间 11 | public enum JJCarousel {} 12 | -------------------------------------------------------------------------------- /Sources/JJCarouselContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJCarouselContainerView.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/13. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 轮播图容器协议 11 | protocol JJCarouselContainerView: UIView { 12 | var delegate: JJCarouselContainerViewDelegate? { get set } 13 | var dataSource: JJCarouselContainerViewDataSource? { get set } 14 | /// 刷新数据 15 | func reload() 16 | /// 需要滑动下一位置 17 | func needAutoScrollToNextIndex() 18 | /// 处理能否手动滑动 19 | func dealManualSliding(enable: Bool) 20 | } 21 | 22 | /// 轮播图容器代理 23 | protocol JJCarouselContainerViewDelegate: NSObjectProtocol { 24 | /// 展示轮播图数据 25 | func displayCell(_ cell: UIView, atIndex index: Int) 26 | /// 点击具体的cell容器 27 | func onClickCell(_ cell: UIView, atIndex index: Int) 28 | /// 将要到index位置 29 | func willScroll(to index: Int) 30 | /// 滑动 31 | func onScroll(from fromIndex: Int, to toIndex: Int, progress: Float) 32 | /// 滑动到index位置 33 | func didScroll(to index: Int) 34 | /// 开始手动拖动scrollView 35 | func scrollViewWillBeginDragging() 36 | /// scrollView停止滑动 37 | func scrollViewDidEndDecelerating() 38 | } 39 | 40 | /// 轮播图容器数据源 41 | protocol JJCarouselContainerViewDataSource: NSObjectProtocol { 42 | /// 轮播方向 43 | func loopDirection() -> JJCarousel.Direction 44 | /// 数据源数量 45 | func numberOfDatas() -> Int 46 | /// 轮播图内边局 47 | func cellContentInset() -> UIEdgeInsets 48 | } 49 | -------------------------------------------------------------------------------- /Sources/JJCarouselView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJCarouselView.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 展示`UIImage`的轮播图别名 11 | public typealias JJLocalImageCarouselView = JJCarouselView 12 | 13 | /// 展示网络图片的轮播图别名 14 | public typealias JJWebImageCarouselView = JJCarouselView 15 | 16 | /// 轮播图组件 17 | public final class JJCarouselView: UIView { 18 | /// 配置 19 | public var config = Config() { 20 | didSet { 21 | containerView.dealManualSliding(enable: config.manualSlidingEnable) 22 | } 23 | } 24 | 25 | /// 数据源 26 | public var datas: [Object] = [] { 27 | didSet { 28 | onGetDatas(old: oldValue, new: datas) 29 | } 30 | } 31 | 32 | /// 事件回调 33 | public var event = Event() 34 | 35 | /// 用于设置轮播图的指示器 36 | public var pageView: JJCarouselViewPageable = JJCarouselDotPageView() { 37 | didSet { 38 | oldValue.removeFromSuperview() 39 | pageView.numberOfPages = datas.count 40 | addSubview(pageView) 41 | bringSubviewToFront(pageView) 42 | } 43 | } 44 | 45 | /// 初始化 46 | /// - Parameters: 47 | /// - frame: frame 48 | /// - initialize: 初始化单个cell的方法。如果`nil`,则按照其初始化方法`init(frame: CGRect)`来初始化 49 | /// - style: 轮播图风格,默认`full`平铺风格 50 | public init(frame: CGRect, initialize: (() -> Cell)?, style: Style = .full) { 51 | containerView = style.createContainerView(frame: frame, initialize: initialize) 52 | super.init(frame: frame) 53 | containerView.dataSource = self 54 | containerView.delegate = self 55 | addSubview(containerView) 56 | addSubview(pageView) 57 | addObservers() 58 | } 59 | 60 | public override init(frame: CGRect) { 61 | containerView = Style.full.createContainerView(frame: .zero, initialize: nil) 62 | super.init(frame: frame) 63 | containerView.dataSource = self 64 | containerView.delegate = self 65 | addSubview(containerView) 66 | addSubview(pageView) 67 | addObservers() 68 | } 69 | 70 | required init?(coder: NSCoder) { 71 | fatalError("init(coder:) has not been implemented") 72 | } 73 | 74 | // MARK: - 组件 75 | private let containerView: JJCarouselContainerView 76 | 77 | // MARK: - 属性 78 | private var preScrollcontentOffset = CGPoint.zero 79 | private var timer: Timer? 80 | private var didEnterBackgroundObserver: NSObjectProtocol? 81 | private var willEnterForegroundObserver: NSObjectProtocol? 82 | 83 | deinit { 84 | removeObservers() 85 | destoryTimer() 86 | } 87 | 88 | public override func willMove(toWindow newWindow: UIWindow?) { 89 | super.willMove(toWindow: newWindow) 90 | if newWindow == nil { 91 | scrollViewWillBeginDragging() 92 | } else { 93 | scrollViewDidEndDecelerating() 94 | } 95 | } 96 | 97 | public override func layoutSubviews() { 98 | super.layoutSubviews() 99 | containerView.frame = bounds 100 | pageView.frame = config.pageViewFrame(pageView, config.direction, bounds.size, datas.count) 101 | } 102 | 103 | // MARK: - 私有方法 104 | 105 | /// 重启定时器 106 | @IBAction private func resumeTimer() { 107 | timer?.fireDate = Date() 108 | } 109 | 110 | @IBAction private func loopTimer_below10() { 111 | if !datas.isEmpty { 112 | containerView.needAutoScrollToNextIndex() 113 | } 114 | } 115 | } 116 | 117 | // MARK: - JJCarouselContainerViewDataSource, JJCarouselContainerViewDelegate 118 | extension JJCarouselView: JJCarouselContainerViewDataSource, JJCarouselContainerViewDelegate { 119 | func loopDirection() -> JJCarousel.Direction { 120 | return config.direction 121 | } 122 | 123 | func numberOfDatas() -> Int { 124 | return datas.count 125 | } 126 | 127 | func cellContentInset() -> UIEdgeInsets { 128 | return config.contentInset 129 | } 130 | 131 | func displayCell(_ cell: UIView, atIndex index: Int) { 132 | if let cell = cell as? Cell { 133 | config.display?(cell, datas[index]) 134 | } 135 | } 136 | 137 | func onClickCell(_ cell: UIView, atIndex index: Int) { 138 | if let cell = cell as? Cell { 139 | event.onTapFunction(view: cell, object: datas[index], index: index) 140 | } 141 | } 142 | 143 | func willScroll(to index: Int) { 144 | event.willMoveFunction(index: index) 145 | } 146 | 147 | func onScroll(from fromIndex: Int, to toIndex: Int, progress: Float) { 148 | event.onScrollFunction(fronIndex: fromIndex, toIndex: toIndex, progress: progress) 149 | pageView.onScroll(from: fromIndex, to: toIndex, progress: progress) 150 | } 151 | 152 | func didScroll(to index: Int) { 153 | pageView.currentPage = index 154 | event.didMoveFunction(index: index) 155 | } 156 | 157 | func scrollViewWillBeginDragging() { 158 | if config.autoLoop { 159 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resumeTimer), object: nil) 160 | pauseTimer() 161 | } 162 | } 163 | 164 | func scrollViewDidEndDecelerating() { 165 | if config.autoLoop { 166 | /// 先取消,再重启....防止多次重启 167 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resumeTimer), object: nil) 168 | perform(#selector(resumeTimer), with: nil, afterDelay: config.loopTimeInterval) 169 | } 170 | } 171 | } 172 | 173 | // MARK: - private 174 | private extension JJCarouselView { 175 | func onGetDatas(old: [Object], new: [Object]) { 176 | if old.count != new.count { 177 | onGetDifferentDatas() 178 | return 179 | } 180 | if new.isEmpty { 181 | return 182 | } 183 | let isSame = new.elementsEqual(old, by: { $0 == $1 }) 184 | if !isSame { 185 | onGetDifferentDatas() 186 | } 187 | } 188 | 189 | func onGetDifferentDatas() { 190 | pageView.numberOfPages = datas.count 191 | if !bounds.isEmpty { 192 | pageView.frame = config.pageViewFrame(pageView, config.direction, bounds.size, datas.count) 193 | } 194 | containerView.reload() 195 | if datas.count > 1 { 196 | createTimer() 197 | } else { 198 | destoryTimer() 199 | } 200 | } 201 | } 202 | 203 | // MARK: - NotificationCenter相关 204 | private extension JJCarouselView { 205 | func addObservers() { 206 | didEnterBackgroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main, using: { [weak self] _ in 207 | self?.scrollViewWillBeginDragging() 208 | }) 209 | willEnterForegroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main, using: { [weak self] _ in 210 | guard let self = self else { 211 | return 212 | } 213 | if self.config.autoLoop && (self.window != nil) { 214 | self.timer?.fireDate = Date().addingTimeInterval(self.config.loopTimeInterval) 215 | } 216 | }) 217 | } 218 | 219 | func removeObservers() { 220 | if let didEnterBackgroundObserver = didEnterBackgroundObserver { 221 | NotificationCenter.default.removeObserver(didEnterBackgroundObserver) 222 | } 223 | if let willEnterForegroundObserver = willEnterForegroundObserver { 224 | NotificationCenter.default.removeObserver(willEnterForegroundObserver) 225 | } 226 | } 227 | } 228 | 229 | // MARK: - timer相关 230 | private extension JJCarouselView { 231 | func createTimer() { 232 | if !config.autoLoop { 233 | return 234 | } 235 | if #available(iOS 10.0, *) { 236 | let timer = Timer(fire: Date().addingTimeInterval(config.loopTimeInterval), interval: config.loopTimeInterval, repeats: true) { [weak self] _ in 237 | guard let self = self, !self.datas.isEmpty else { 238 | return 239 | } 240 | self.containerView.needAutoScrollToNextIndex() 241 | } 242 | self.timer = timer 243 | RunLoop.current.add(timer, forMode: .common) 244 | } else { 245 | let timer = Timer(fireAt: Date().addingTimeInterval(config.loopTimeInterval), interval: config.loopTimeInterval, target: self, selector: #selector(loopTimer_below10), userInfo: nil, repeats: true) 246 | self.timer = timer 247 | RunLoop.current.add(timer, forMode: .common) 248 | } 249 | } 250 | 251 | func pauseTimer() { 252 | timer?.fireDate = .distantFuture 253 | } 254 | 255 | func destoryTimer() { 256 | timer?.invalidate() 257 | timer = nil 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Sources/JJCarouselViewPageable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJCarouselViewPageable.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 抽象化轮播图page指示器协议 11 | public protocol JJCarouselViewPageable: UIView { 12 | /// 如果只有一个轮播数据时,是否隐藏page指示器 13 | var hidesForSinglePage: Bool { get set } 14 | 15 | /// 轮播数据总数 16 | var numberOfPages: Int { get set } 17 | 18 | /// 当前位置 19 | var currentPage: Int { get set } 20 | 21 | /// 非当前指示器颜色 22 | var pageIndicatorTintColor: UIColor? { get set } 23 | 24 | /// 当前指示器颜色 25 | var currentPageIndicatorTintColor: UIColor? { get set } 26 | 27 | /// 根据给定的数据数量计算对应控件所需的size 28 | func size(forNumberOfPages pageCount: Int) -> CGSize 29 | 30 | /// 轮播图滑动回调 31 | /// - Parameters: 32 | /// - fromIndex: 当前index 33 | /// - toindex: 目标index 34 | /// - progress: 进度 35 | func onScroll(from fromIndex: Int, to toindex: Int, progress: Float) 36 | } 37 | 38 | extension UIPageControl: JJCarouselViewPageable { 39 | public func onScroll(from fromIndex: Int, to toindex: Int, progress: Float) {} 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Providers/FullCarouselContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullCarouselContainerView.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/13. 6 | // 7 | 8 | import UIKit 9 | 10 | extension JJCarouselView { 11 | /// cell充满view风格轮播图 12 | final class FullContainerView: UIView, JJCarouselContainerView, UIScrollViewDelegate { 13 | weak var delegate: JJCarouselContainerViewDelegate? 14 | weak var dataSource: JJCarouselContainerViewDataSource? 15 | 16 | init(frame: CGRect, initialize: (() -> Cell)?) { 17 | scrollView = UIScrollView(frame: CGRect(origin: .zero, size: frame.size)) 18 | scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 19 | scrollView.isPagingEnabled = true 20 | scrollView.bounces = false 21 | scrollView.showsVerticalScrollIndicator = false 22 | scrollView.showsHorizontalScrollIndicator = false 23 | if #available(iOS 11.0, *) { 24 | scrollView.contentInsetAdjustmentBehavior = .never 25 | } else { 26 | // Fallback on earlier versions 27 | } 28 | firstContainer = CellContainer(cell: initialize?() ?? Cell(frame: .zero), index: 0) 29 | secondContainer = CellContainer(cell: initialize?() ?? Cell(frame: .zero), index: 1) 30 | thirdContainer = CellContainer(cell: initialize?() ?? Cell(frame: .zero), index: 2) 31 | super.init(frame: CGRect(origin: .zero, size: frame.size)) 32 | scrollView.isScrollEnabled = false 33 | scrollView.delegate = self 34 | addSubview(scrollView) 35 | [firstContainer, secondContainer, thirdContainer].forEach { [unowned self] obj in 36 | obj.cell.isHidden = true 37 | obj.onTap = { [weak self] cell, idx in 38 | self?.delegate?.onClickCell(cell, atIndex: idx) 39 | } 40 | self.scrollView.addSubview(obj.cell) 41 | } 42 | } 43 | 44 | required init?(coder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | // MARK: - 组件 49 | private let scrollView: UIScrollView 50 | private let firstContainer: CellContainer 51 | private let secondContainer: CellContainer 52 | private let thirdContainer: CellContainer 53 | 54 | // MARK: - 属性 55 | private var currentFrame = CGRect.zero { 56 | didSet { 57 | onChangeFrame(old: oldValue, new: currentFrame) 58 | } 59 | } 60 | 61 | private var currentIndex = 0 { 62 | didSet { 63 | onChangeCurrentIndex() 64 | } 65 | } 66 | 67 | private var willScrollIndex = -1 { 68 | didSet { 69 | if (willScrollIndex != oldValue) && (willScrollIndex != -1) { 70 | delegate?.willScroll(to: willScrollIndex) 71 | } 72 | } 73 | } 74 | 75 | private var manualSlidingEnable = true 76 | 77 | override func layoutSubviews() { 78 | super.layoutSubviews() 79 | currentFrame = bounds 80 | } 81 | 82 | // MARK: - UIScrollViewDelegate 83 | 84 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 85 | let dataCount = dataSource?.numberOfDatas() ?? 0 86 | if (bounds.width == 0) || (bounds.height == 0) || (dataCount == 0) { 87 | return 88 | } 89 | let direction = dataSource?.loopDirection() ?? .ltr 90 | switch direction { 91 | case .ltr: 92 | let offsetX = scrollView.contentOffset.x 93 | if (bounds.width < offsetX) && (offsetX < bounds.width * 2) { 94 | let nextIndex = (currentIndex + 1) % dataCount 95 | willScrollIndex = nextIndex 96 | let progress = (offsetX - bounds.width) / bounds.width 97 | delegate?.onScroll(from: currentIndex, to: nextIndex, progress: Float(progress)) 98 | } else if (0 < offsetX) && (offsetX < bounds.width) { 99 | let preIndex = (currentIndex - 1 + dataCount) % dataCount 100 | willScrollIndex = preIndex 101 | let progress = (bounds.width - offsetX) / bounds.width 102 | delegate?.onScroll(from: currentIndex, to: preIndex, progress: Float(progress)) 103 | } 104 | case .rtl: 105 | let offsetX = scrollView.contentOffset.x 106 | if (bounds.width < offsetX) && (offsetX < bounds.width * 2) { 107 | let nextIndex = (currentIndex - 1 + dataCount) % dataCount 108 | willScrollIndex = nextIndex 109 | let progress = (offsetX - bounds.width) / bounds.width 110 | delegate?.onScroll(from: currentIndex, to: nextIndex, progress: Float(progress)) 111 | } else if (0 < offsetX) && (offsetX < bounds.width) { 112 | let preIndex = (currentIndex + 1 + dataCount) % dataCount 113 | willScrollIndex = preIndex 114 | let progress = (bounds.width - offsetX) / bounds.width 115 | delegate?.onScroll(from: currentIndex, to: preIndex, progress: Float(progress)) 116 | } 117 | case .ttb: 118 | let offsetY = scrollView.contentOffset.y 119 | if (bounds.height < offsetY) && (offsetY < bounds.height * 2) { 120 | let nextIndex = (currentIndex + 1) % dataCount 121 | willScrollIndex = nextIndex 122 | let progress = (offsetY - bounds.height) / bounds.height 123 | delegate?.onScroll(from: currentIndex, to: nextIndex, progress: Float(progress)) 124 | } else if (0 < offsetY) && (offsetY < bounds.height) { 125 | let preIndex = (currentIndex - 1 + dataCount) % dataCount 126 | willScrollIndex = preIndex 127 | let progress = (bounds.height - offsetY) / bounds.height 128 | delegate?.onScroll(from: currentIndex, to: preIndex, progress: Float(progress)) 129 | } 130 | case .btt: 131 | let offsetY = scrollView.contentOffset.y 132 | if (bounds.height < offsetY) && (offsetY < bounds.height * 2) { 133 | let nextIndex = (currentIndex - 1 + dataCount) % dataCount 134 | willScrollIndex = nextIndex 135 | let progress = (offsetY - bounds.height) / bounds.height 136 | delegate?.onScroll(from: currentIndex, to: nextIndex, progress: Float(progress)) 137 | } else if (0 < offsetY) && (offsetY < bounds.height) { 138 | let preIndex = (currentIndex + 1 + dataCount) % dataCount 139 | willScrollIndex = preIndex 140 | let progress = (bounds.height - offsetY) / bounds.height 141 | delegate?.onScroll(from: currentIndex, to: preIndex, progress: Float(progress)) 142 | } 143 | } 144 | } 145 | 146 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 147 | delegate?.scrollViewWillBeginDragging() 148 | } 149 | 150 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 151 | adjustWhenEndScrolling(animation: false) 152 | } 153 | 154 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 155 | adjustWhenEndScrolling(animation: true) 156 | } 157 | 158 | func reload() { 159 | onGetDatas() 160 | } 161 | 162 | func needAutoScrollToNextIndex() { 163 | let direction = dataSource?.loopDirection() ?? .ltr 164 | switch direction { 165 | case .ltr: 166 | scrollView.setContentOffset(CGPoint(x: bounds.width * 2, y: 0), animated: true) 167 | case .rtl: 168 | scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true) 169 | case .ttb: 170 | scrollView.setContentOffset(CGPoint(x: 0, y: bounds.height * 2), animated: true) 171 | case .btt: 172 | scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true) 173 | } 174 | } 175 | 176 | func dealManualSliding(enable: Bool) { 177 | manualSlidingEnable = enable 178 | let dataCount = dataSource?.numberOfDatas() ?? 0 179 | scrollView.isScrollEnabled = enable ? (dataCount > 1) : false 180 | } 181 | } 182 | } 183 | 184 | private extension JJCarouselView.FullContainerView { 185 | func onChangeFrame(old: CGRect, new: CGRect) { 186 | if old == new { 187 | return 188 | } 189 | layoutCells() 190 | let direction = dataSource?.loopDirection() ?? .ltr 191 | switch direction { 192 | case .ltr, .rtl: 193 | scrollView.contentSize = CGSize(width: bounds.width * 3, height: bounds.height) 194 | scrollView.contentOffset = CGPoint(x: bounds.width, y: 0) 195 | case .ttb, .btt: 196 | scrollView.contentSize = CGSize(width: bounds.width, height: bounds.height * 3) 197 | scrollView.contentOffset = CGPoint(x: 0, y: bounds.height) 198 | } 199 | } 200 | 201 | func layoutCells() { 202 | let contentInset = dataSource?.cellContentInset() ?? .zero 203 | let direction = dataSource?.loopDirection() ?? .ltr 204 | switch direction { 205 | case .ltr, .rtl: 206 | let contentInsetWidth = bounds.width - contentInset.left - contentInset.right 207 | let cellWidth = contentInsetWidth > 0 ? contentInsetWidth : 0 208 | let contentInsetHeight = bounds.height - contentInset.top - contentInset.bottom 209 | let cellHeight = contentInsetHeight > 0 ? contentInsetHeight : 0 210 | firstContainer.cell.frame = CGRect(x: contentInset.left, y: contentInset.top, width: cellWidth, height: cellHeight) 211 | secondContainer.cell.frame = firstContainer.cell.frame.offsetBy(dx: bounds.width, dy: 0) 212 | thirdContainer.cell.frame = secondContainer.cell.frame.offsetBy(dx: bounds.width, dy: 0) 213 | case .ttb, .btt: 214 | firstContainer.cell.frame = CGRect(origin: .zero, size: bounds.size) 215 | let contentInsetWidth = bounds.width - contentInset.left - contentInset.right 216 | let cellWidth = contentInsetWidth > 0 ? contentInsetWidth : 0 217 | let contentInsetHeight = bounds.height - contentInset.top - contentInset.bottom 218 | let cellHeight = contentInsetHeight > 0 ? contentInsetHeight : 0 219 | firstContainer.cell.frame = CGRect(x: contentInset.left, y: contentInset.top, width: cellWidth, height: cellHeight) 220 | secondContainer.cell.frame = firstContainer.cell.frame.offsetBy(dx: 0, dy: bounds.height) 221 | thirdContainer.cell.frame = secondContainer.cell.frame.offsetBy(dx: 0, dy: bounds.height) 222 | } 223 | } 224 | 225 | func onGetDatas() { 226 | let dataCount = dataSource?.numberOfDatas() ?? 0 227 | scrollView.isScrollEnabled = manualSlidingEnable ? (dataCount > 1) : false 228 | switch dataCount { 229 | case 0: 230 | [firstContainer, secondContainer, thirdContainer].forEach { $0.cell.isHidden = true } 231 | case 1: 232 | [firstContainer, thirdContainer].forEach { $0.cell.isHidden = true } 233 | secondContainer.cell.isHidden = false 234 | default: 235 | [firstContainer, secondContainer, thirdContainer].forEach { $0.cell.isHidden = false } 236 | } 237 | if bounds.isEmpty { 238 | setNeedsLayout() 239 | layoutIfNeeded() 240 | } else { 241 | let direction = dataSource?.loopDirection() ?? .ltr 242 | switch direction { 243 | case .ltr, .rtl: 244 | scrollView.contentOffset = CGPoint(x: bounds.width, y: 0) 245 | case .ttb, .btt: 246 | scrollView.contentOffset = CGPoint(x: 0, y: bounds.height) 247 | } 248 | } 249 | delegate?.willScroll(to: 0) 250 | currentIndex = 0 251 | } 252 | 253 | func onChangeCurrentIndex() { 254 | let dataCount = dataSource?.numberOfDatas() ?? 0 255 | delegate?.didScroll(to: currentIndex) 256 | switch dataCount { 257 | case 0: 258 | return 259 | case 1: 260 | secondContainer.index = 0 261 | delegate?.displayCell(secondContainer.cell, atIndex: 0) 262 | default: 263 | let direction = dataSource?.loopDirection() ?? .ltr 264 | switch direction { 265 | case .ltr, .ttb: 266 | let preIndex = (currentIndex - 1 + dataCount) % dataCount 267 | firstContainer.index = preIndex 268 | delegate?.displayCell(firstContainer.cell, atIndex: preIndex) 269 | secondContainer.index = currentIndex 270 | delegate?.displayCell(secondContainer.cell, atIndex: currentIndex) 271 | let nextIndex = (currentIndex + 1) % dataCount 272 | thirdContainer.index = nextIndex 273 | delegate?.displayCell(thirdContainer.cell, atIndex: nextIndex) 274 | case .rtl, .btt: 275 | let preIndex = (currentIndex + 1 + dataCount) % dataCount 276 | firstContainer.index = preIndex 277 | delegate?.displayCell(firstContainer.cell, atIndex: preIndex) 278 | secondContainer.index = currentIndex 279 | delegate?.displayCell(secondContainer.cell, atIndex: currentIndex) 280 | let nextIndex = (currentIndex - 1 + dataCount) % dataCount 281 | thirdContainer.index = nextIndex 282 | delegate?.displayCell(thirdContainer.cell, atIndex: nextIndex) 283 | } 284 | } 285 | } 286 | 287 | func adjustWhenEndScrolling(animation: Bool) { 288 | let dataCount = dataSource?.numberOfDatas() ?? 0 289 | if dataCount == 0 { 290 | return 291 | } 292 | if !animation { 293 | delegate?.scrollViewDidEndDecelerating() 294 | } 295 | let offset = scrollView.contentOffset 296 | let storeCurrentIndex = currentIndex 297 | var nextIndex = currentIndex 298 | let direction = dataSource?.loopDirection() ?? .ltr 299 | switch direction { 300 | case .ltr: 301 | if offset.x == 0 { 302 | nextIndex = (currentIndex - 1 + dataCount) % dataCount 303 | scrollView.contentOffset = CGPoint(x: bounds.width, y: 0) 304 | } else if offset.x == bounds.width * 2 { 305 | nextIndex = (currentIndex + 1 + dataCount) % dataCount 306 | scrollView.contentOffset = CGPoint(x: bounds.width, y: 0) 307 | } 308 | case .rtl: 309 | if offset.x == 0 { 310 | nextIndex = (currentIndex + 1 + dataCount) % dataCount 311 | scrollView.contentOffset = CGPoint(x: bounds.width, y: 0) 312 | } else if offset.x == bounds.width * 2 { 313 | nextIndex = (currentIndex - 1 + dataCount) % dataCount 314 | scrollView.contentOffset = CGPoint(x: bounds.width, y: 0) 315 | } 316 | case .ttb: 317 | if offset.y == 0 { 318 | nextIndex = (currentIndex - 1 + dataCount) % dataCount 319 | scrollView.contentOffset = CGPoint(x: 0, y: bounds.height) 320 | } else if offset.y == bounds.height * 2 { 321 | nextIndex = (currentIndex + 1 + dataCount) % dataCount 322 | scrollView.contentOffset = CGPoint(x: 0, y: bounds.height) 323 | } 324 | case .btt: 325 | if offset.y == 0 { 326 | nextIndex = (currentIndex + 1 + dataCount) % dataCount 327 | scrollView.contentOffset = CGPoint(x: 0, y: bounds.height) 328 | } else if offset.y == bounds.height * 2 { 329 | nextIndex = (currentIndex - 1 + dataCount) % dataCount 330 | scrollView.contentOffset = CGPoint(x: 0, y: bounds.height) 331 | } 332 | } 333 | delegate?.onScroll(from: storeCurrentIndex, to: nextIndex, progress: 1.0) 334 | willScrollIndex = -1 335 | currentIndex = nextIndex 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /Sources/Providers/JJCarouselDotPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJCarouselDotPageView.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 圆点轮播图page指示器。 11 | public final class JJCarouselDotPageView: UIView, JJCarouselViewPageable { 12 | public var hidesForSinglePage: Bool = true 13 | public var numberOfPages: Int = 0 { 14 | didSet { 15 | isHidden = (numberOfPages > 1) ? false : hidesForSinglePage 16 | if numberOfPages != oldValue { 17 | dotLayers.forEach { $0.removeFromSuperlayer() } 18 | dotLayers = [] 19 | createDotLayers() 20 | } 21 | } 22 | } 23 | 24 | public var currentPage: Int = 0 { 25 | didSet { 26 | if currentPage != oldValue { 27 | resetDotViewsColor() 28 | } 29 | } 30 | } 31 | 32 | public var pageIndicatorTintColor: UIColor? = UIColor.gray 33 | 34 | public var currentPageIndicatorTintColor: UIColor? = UIColor.white 35 | 36 | /// 圆点大小 37 | public var dotViewSize = CGSize(width: 6, height: 6) 38 | 39 | /// 圆点间距 40 | public var dotSpace: CGFloat = 4 41 | 42 | /// 轮播图滑动时,是否使用过渡混合色。默认`true` 43 | public var usingTransitionColorWhileScroll = true 44 | 45 | public func size(forNumberOfPages pageCount: Int) -> CGSize { 46 | return sizeThatFits(.zero) 47 | } 48 | 49 | public func onScroll(from fromIndex: Int, to toindex: Int, progress: Float) { 50 | if !usingTransitionColorWhileScroll { 51 | return 52 | } 53 | let currentColor = currentPageIndicatorTintColor ?? .clear 54 | let pageColor = pageIndicatorTintColor ?? .clear 55 | let fc = currentColor.mix(secondColor: pageColor, secondColorRatio: progress) 56 | dotLayers[fromIndex].backgroundColor = fc.cgColor 57 | let tc = pageColor.mix(secondColor: currentColor, secondColorRatio: progress) 58 | dotLayers[toindex].backgroundColor = tc.cgColor 59 | } 60 | 61 | public override func sizeThatFits(_ size: CGSize) -> CGSize { 62 | if numberOfPages <= 1 { 63 | return dotViewSize 64 | } 65 | let w = (dotViewSize.width + dotSpace) * CGFloat(numberOfPages) - dotSpace 66 | return CGSize(width: w, height: dotViewSize.height) 67 | } 68 | 69 | public override func layoutSubviews() { 70 | super.layoutSubviews() 71 | let ty = (bounds.height - dotViewSize.height) * 0.5 72 | for (idx, pl) in dotLayers.enumerated() { 73 | pl.frame = CGRect(x: (dotViewSize.width + dotSpace) * CGFloat(idx), y: ty, width: dotViewSize.width, height: dotViewSize.height) 74 | } 75 | } 76 | 77 | private var dotLayers: [CALayer] = [] 78 | 79 | private func createDotLayers() { 80 | if numberOfPages <= 0 { 81 | return 82 | } 83 | for idx in 0.. CGSize { 23 | return .zero 24 | } 25 | 26 | public func onScroll(from fromIndex: Int, to toindex: Int, progress: Float) { } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Providers/JJCarouselNumberPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJCarouselNumberPageView.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/8. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 数字化轮播图page指示器。具体变现为: 1/6 11 | public final class JJCarouselNumberPageView: UIView, JJCarouselViewPageable { 12 | public var hidesForSinglePage: Bool = true 13 | 14 | public var numberOfPages: Int = 0 { 15 | didSet { 16 | isHidden = (numberOfPages > 1) ? false : hidesForSinglePage 17 | totalLabel.text = "\(numberOfPages)" 18 | totalLabel.sizeToFit() 19 | } 20 | } 21 | 22 | public var currentPage: Int = 0 { 23 | didSet { 24 | currentLabel.text = "\(currentPage + 1)" 25 | currentLabel.sizeToFit() 26 | } 27 | } 28 | 29 | public var pageIndicatorTintColor: UIColor? = .white { 30 | didSet { 31 | totalLabel.textColor = pageIndicatorTintColor 32 | } 33 | } 34 | 35 | public var dotTintColor: UIColor? = UIColor.white { 36 | didSet { 37 | dotLabel.textColor = dotTintColor 38 | } 39 | } 40 | 41 | public var currentPageIndicatorTintColor: UIColor? = .white { 42 | didSet { 43 | currentLabel.textColor = currentPageIndicatorTintColor 44 | } 45 | } 46 | 47 | /// 字体大小 48 | public var font = UIFont.systemFont(ofSize: 11) { 49 | didSet { 50 | dotLabel.font = font 51 | currentLabel.font = font 52 | totalLabel.font = font 53 | currentLabel.sizeToFit() 54 | dotLabel.sizeToFit() 55 | totalLabel.sizeToFit() 56 | } 57 | } 58 | 59 | override init(frame: CGRect) { 60 | super.init(frame: frame) 61 | setup() 62 | } 63 | 64 | required init?(coder: NSCoder) { 65 | super.init(coder: coder) 66 | setup() 67 | } 68 | 69 | private lazy var currentLabel = UILabel() 70 | private lazy var dotLabel = UILabel() 71 | private lazy var totalLabel = UILabel() 72 | 73 | public func size(forNumberOfPages pageCount: Int) -> CGSize { 74 | return sizeThatFits(.zero) 75 | } 76 | 77 | public func onScroll(from fromIndex: Int, to toindex: Int, progress: Float) { } 78 | 79 | public override func sizeThatFits(_ size: CGSize) -> CGSize { 80 | let w = dotLabel.bounds.width + totalLabel.bounds.width * 2 81 | let h = max(currentLabel.bounds.height, totalLabel.bounds.width) 82 | return CGSize(width: w + 16, height: h + 8) 83 | } 84 | 85 | public override func layoutSubviews() { 86 | super.layoutSubviews() 87 | layer.cornerRadius = bounds.height * 0.5 88 | 89 | dotLabel.frame = CGRect(x: (bounds.width - dotLabel.bounds.width) * 0.5, y: (bounds.height - dotLabel.bounds.height) * 0.5, width: dotLabel.bounds.width, height: dotLabel.bounds.height) 90 | 91 | currentLabel.frame = CGRect(x: dotLabel.frame.minX - currentLabel.frame.width - 2, y: (bounds.height - currentLabel.bounds.height) * 0.5, width: currentLabel.bounds.width, height: currentLabel.bounds.height) 92 | 93 | totalLabel.frame = CGRect(x: dotLabel.frame.maxX + 2, y: (bounds.height - totalLabel.bounds.height) * 0.5, width: totalLabel.bounds.width, height: totalLabel.bounds.height) 94 | } 95 | 96 | private func setup() { 97 | dotLabel.text = "/" 98 | currentLabel.text = " " 99 | totalLabel.text = " " 100 | pageIndicatorTintColor = .white 101 | currentPageIndicatorTintColor = .white 102 | dotTintColor = .white 103 | font = UIFont.systemFont(ofSize: 11) 104 | currentLabel.textAlignment = .right 105 | totalLabel.textAlignment = .left 106 | dotLabel.sizeToFit() 107 | currentLabel.sizeToFit() 108 | totalLabel.sizeToFit() 109 | [currentLabel, dotLabel, totalLabel].forEach { self.addSubview($0) } 110 | clipsToBounds = true 111 | backgroundColor = UIColor.black.withAlphaComponent(0.4) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Style.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Style.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/13. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension JJCarouselView { 11 | /// 轮播图风格 12 | public enum Style { 13 | /// 平铺式-----单个cell充满整个轮播图容器 14 | case full 15 | // TODO: - 其它风格 16 | } 17 | } 18 | 19 | extension JJCarouselView.Style { 20 | func createContainerView(frame: CGRect, initialize: (() -> Cell)?) -> JJCarouselContainerView { 21 | switch self { 22 | case .full: 23 | return JJCarouselView.FullContainerView(frame: frame, initialize: initialize) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // JJCarouselView 4 | // 5 | // Created by zgjff on 2022/4/21. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | func mix(secondColor color: UIColor, secondColorRatio ratio: Float) -> UIColor { 12 | let rio = CGFloat(ratio > 1 ? 1 : (ratio < 0) ? 0 : ratio) 13 | 14 | var fr: CGFloat = 0 15 | var fg: CGFloat = 0 16 | var fb: CGFloat = 0 17 | var fa: CGFloat = 0 18 | 19 | var sr: CGFloat = 0 20 | var sg: CGFloat = 0 21 | var sb: CGFloat = 0 22 | var sa: CGFloat = 0 23 | 24 | guard getRed(&fr, green: &fg, blue: &fb, alpha: &fa), 25 | color.getRed(&sr, green: &sg, blue: &sb, alpha: &sa) else { 26 | return color 27 | } 28 | let r = fr * (1 - rio) + sr * rio 29 | let g = fg * (1 - rio) + sg * rio 30 | let b = fb * (1 - rio) + sb * rio 31 | let a = fa * (1 - rio) + sa * rio 32 | return UIColor(red: r, green: g, blue: b, alpha: a) 33 | } 34 | 35 | static func random() -> UIColor { 36 | return UIColor(hue: CGFloat(arc4random() % 256) / 256.0, saturation: CGFloat(arc4random() % 128) / 256.0 + 0.5, brightness: CGFloat(arc4random() % 128) / 256.0 + 0.5, alpha: 1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/JJCarouselViewTests/JJCarouselViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JJCarouselView 3 | 4 | final class JJCarouselViewTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(JJCarouselView().text, "Hello, World!") 10 | } 11 | } 12 | --------------------------------------------------------------------------------