├── CWCarouselDemo ├── oc_demo.png ├── CWCarousel │ ├── Sources │ │ ├── 01.jpg │ │ ├── 02.jpg │ │ ├── 03.jpg │ │ ├── 04.jpg │ │ ├── 05.jpg │ │ ├── main.m │ │ ├── Info.plist │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ └── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Swift │ │ ├── CWCarousel-Bridging-Header.h │ │ └── CWProxy.swift │ ├── ViewController.h │ ├── AppDelegate.h │ ├── code │ │ ├── CWPageControl.h │ │ └── CWPageControl.m │ ├── AppDelegate.m │ ├── SViewController.swift │ ├── TestController.swift │ ├── ShowViewController.swift │ └── ViewController.m └── CWCarousel.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── OC_CWCarousel ├── CWCarouselHeader.h ├── CWFlowLayout.h ├── CWCarouselProtocol.h ├── CWCarousel.h ├── CWFlowLayout.m └── CWCarousel.m ├── OC_CWCarousel.podspec ├── LICENSE ├── .gitignore ├── README.md └── Swift_CWCarousel ├── CWSwiftFlowLayout.swift └── CWBanner.swift /CWCarouselDemo/oc_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/oc_demo.png -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/01.jpg -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/02.jpg -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/03.jpg -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/04.jpg -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/05.jpg -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Swift/CWCarousel-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OC_CWCarousel/CWCarouselHeader.h: -------------------------------------------------------------------------------- 1 | // 2 | // CWCarouselHeader.h 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #ifndef CWCarouselHeader_h 10 | #define CWCarouselHeader_h 11 | 12 | #import "CWCarousel.h" 13 | #import "CWCarouselProtocol.h" 14 | 15 | #endif /* CWCarouselHeader_h */ 16 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/code/CWPageControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // CWPageControl.h 3 | // CWCarousel 4 | // 5 | // Created by chenwang on 2018/7/16. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CWCarouselProtocol.h" 11 | @interface CWPageControl : UIView 12 | + (CGFloat)widthFromNumber:(NSInteger)num; 13 | @end 14 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /OC_CWCarousel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "OC_CWCarousel" 3 | s.version = "1.1.9" 4 | s.summary = "banner. 无限滚动轮播图" 5 | s.homepage = "https://github.com/baozoudiudiu/CWCarousel" 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { "baozoudiudiu" => "diudiu_WangChen@163.com" } 8 | s.source = { :git => "https://github.com/baozoudiudiu/CWCarousel.git", 9 | :tag => "#{s.version}" } 10 | s.platform = :ios, '9.0' 11 | s.requires_arc = true 12 | s.source_files = 'OC_CWCarousel/*' 13 | s.frameworks = 'Foundation', 'UIKit' 14 | end 15 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Swift/CWProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CWProxy.swift 3 | // CWCarousel 4 | // 5 | // Created by 陈旺 on 2021/10/28. 6 | // Copyright © 2021 ChenWang. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CWProxy { 12 | 13 | typealias TimerHandler = (T?) -> Void 14 | 15 | private(set) weak var target: T? 16 | 17 | fileprivate var handler: TimerHandler? 18 | 19 | init(_ target: T?) { 20 | self.target = target 21 | } 22 | 23 | func createTimer(timeInterval: TimeInterval, _ action: TimerHandler?) -> Timer? { 24 | self.handler = action 25 | return Timer.scheduledTimer(timeInterval: timeInterval, 26 | target: self, 27 | selector: #selector(timerAction), 28 | userInfo: nil, 29 | repeats: true) 30 | } 31 | 32 | @objc fileprivate func timerAction() { 33 | self.handler?(self.target) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 baozoudiudiu 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 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | *.DS_Store 31 | 32 | # CocoaPods 33 | # 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 37 | # 38 | # Pods/ 39 | 40 | # Carthage 41 | # 42 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 43 | # Carthage/Checkouts 44 | 45 | Carthage/Build 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 53 | 54 | fastlane/report.xml 55 | fastlane/Preview.html 56 | fastlane/screenshots 57 | fastlane/test_output 58 | 59 | # Code Injection 60 | # 61 | # After new code Injection tools there's a generated folder /iOSInjectionProject 62 | # https://github.com/johnno1962/injectionforxcode 63 | 64 | iOSInjectionProject/ 65 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OC_CWCarousel/CWFlowLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // CWFlowLayout.h 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, CWCarouselStyle) { 12 | CWCarouselStyle_Unknow = 0, ///<未知样式 13 | CWCarouselStyle_Normal, ///<普通样式,一张图占用整个屏幕宽度 14 | CWCarouselStyle_H_1, ///<自定义样式一, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到 15 | CWCarouselStyle_H_2, ///<自定义样式二, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,并且中间一张图正常大小,前后2张图会缩放 16 | CWCarouselStyle_H_3, ///<自定义样式三, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,中间一张有放大效果,前后2张正常大小 17 | }; 18 | 19 | @interface CWFlowLayout : UICollectionViewFlowLayout 20 | /** 21 | 影响轮播图风格 22 | */ 23 | @property (nonatomic, assign) CWCarouselStyle style; 24 | 25 | /** 26 | * 横向滚动时,每张轮播图之间的间距 27 | * CWCarouselStyle_H_3 样式时请设置负值 28 | */ 29 | @property (nonatomic, assign) CGFloat itemSpace_H; 30 | 31 | /** 32 | * 横向滚动时,每张轮播图的宽度 33 | * style = CWCarouselStyle_Normal 时设置无效 34 | */ 35 | @property (nonatomic, assign) CGFloat itemWidth; 36 | 37 | /** 38 | * style = CWCarouselStyle_H_2, CWCarouselStyle_H_3 有效 39 | * 前后2张图的缩小比例 (0.0 ~ 1.0) 40 | * 默认: 0.8 41 | */ 42 | @property (nonatomic, assign) CGFloat minScale; 43 | 44 | /** 45 | * (注: 1.1.7 版本后该属性已经废弃, 设置该属性将不生效. 请使用 minScale代替.) 46 | * style = CWCarouselStyle_H_3 有效 47 | * 中间一张图放大比例 48 | * 默认: 1.2 49 | * 1.1.0版本后,无论设置多少,中间一张的cell的比例始终是原始size, 这个比例是相对两边cell的size的相对比例 50 | 也就是说,该值越大,那么两边的cell就会相对越小.反之越大. 51 | */ 52 | @property (nonatomic, assign) CGFloat maxScale; 53 | 54 | /** 55 | 纵向滚动时,每张轮播图之间的间距(暂未实现) 56 | */ 57 | @property (nonatomic, assign) CGFloat itemSpace_V; 58 | 59 | 60 | /** 61 | 构造方法 62 | 63 | @param style 轮播图风格 64 | @return 实例对象 65 | */ 66 | - (instancetype)initWithStyle:(CWCarouselStyle)style; 67 | @end 68 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/SViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SViewController.swift 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SViewController : UIViewController { 12 | override func viewDidLoad() { 13 | self.configureUI() 14 | } 15 | 16 | private func configureUI () { 17 | self.view.backgroundColor = UIColor.white 18 | self.automaticallyAdjustsScrollViewInsets = false; 19 | self.listView.tableFooterView = UIView.init() 20 | } 21 | 22 | override func didReceiveMemoryWarning() { 23 | 24 | } 25 | 26 | //MARK: - Property 27 | @IBOutlet weak var listView: UITableView! 28 | /// 选项数据源 29 | let titles = ["默认样式", 30 | "可以看到前后两张(正常样式)", 31 | "可以看到前后两张(两边缩放)", 32 | "可以看到前后两张(中间一张放大)"] 33 | 34 | let types = [CWBannerStyle.normal, 35 | CWBannerStyle.preview_normal, 36 | CWBannerStyle.preview_zoom, 37 | CWBannerStyle.preview_big] 38 | } 39 | 40 | extension SViewController: UITableViewDelegate, UITableViewDataSource { 41 | // cell selected 42 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 43 | tableView.deselectRow(at: indexPath, animated: true) 44 | let type = self.types[indexPath.row] 45 | let showVC = ShowViewController.init(style: type) 46 | showVC.hidesBottomBarWhenPushed = true 47 | self.navigationController?.pushViewController(showVC, animated: true) 48 | } 49 | 50 | // cell numbers 51 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 52 | return self.titles.count 53 | } 54 | 55 | // cells 56 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 57 | let idStr = "cellId" 58 | var cell = tableView.dequeueReusableCell(withIdentifier: idStr) 59 | if cell == nil { 60 | cell = UITableViewCell.init(style: .default, reuseIdentifier: idStr) 61 | cell?.accessoryType = .disclosureIndicator 62 | } 63 | cell?.textLabel?.text = self.titles[indexPath.row] 64 | return cell! 65 | } 66 | 67 | // cell height 68 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 69 | return 44 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /OC_CWCarousel/CWCarouselProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // CWCarouselProtocol.h 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #ifndef CWCarouselProtocol_h 10 | #define CWCarouselProtocol_h 11 | 12 | @class CWCarousel; 13 | @protocol CWCarouselDelegate 14 | /** 15 | 轮播图点击代理 16 | 17 | @param carousel 轮播图实例对象 18 | @param index 被点击的下标 19 | */ 20 | - (void)CWCarousel:(CWCarousel *)carousel didSelectedAtIndex:(NSInteger)index; 21 | 22 | @optional 23 | 24 | /** 25 | 将要开始滑动时,会触发该代理 26 | 27 | @param carousel 轮播图控件 28 | @param index 开始滑动时,处于中心点图片的下标 29 | @param indexPathRow 开始滑动时,处于中心点图片的在控件内部的实际下标 30 | */ 31 | - (void)CWCarousel:(CWCarousel *)carousel didStartScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow; 32 | 33 | 34 | /** 35 | 滑动结束后,会触发该代理 36 | 37 | @param carousel 轮播图控件 38 | @param index 结束滑动时,处于中心点图片的下标 39 | @param indexPathRow 结束滑动时,处于中心点图片在控件内部的实际下标 40 | */ 41 | - (void)CWCarousel:(CWCarousel *)carousel didEndScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow; 42 | 43 | 44 | /// 开始布局pageControl时会触发该回调 45 | /// @param carousel 轮播组件对象 46 | /// @param pageControl pageControl对象 47 | /// @param isDefault YES: 组件内部默认的UIPageControl, NO: 自定义的customPageControl 48 | /// 49 | /// 可以在这个回调中自定义pageControl的布局, 如果没有实现该回调, 将默认在底部居中. 50 | /// 51 | /// 注意: 52 | /// 1. 先设置customPageControl, 后将CWCarousel添加到父视图中, 此回调只会被调用一次. 53 | /// 2. 先将CWCarousel添加到父视图中, 后设置customPageControl, 此回调会被调用两次, 第一次isDefault = YES, 第二次isDefault = NO. 因为将CWCarousel添加到父视图时检测到没有customPageControl, 组件内部并不清楚外部调用者是否会设置customPageControl, 会先创建并添加默认的pageControl. 54 | /// 3. 当自定义的customPageControl宽度布局不是自己撑开的时候, 请在该回调中自己布局. 否则采用默认的布局将不可见. 55 | - (void)CWCarousel:(CWCarousel *)carousel addPageControl:(UIView *)pageControl isDefault:(BOOL)isDefault; 56 | 57 | @end 58 | 59 | @protocol CWCarouselDatasource 60 | /** 61 | 轮播图数量 62 | 63 | @return 轮播图展示个数 64 | */ 65 | - (NSInteger)numbersForCarousel; 66 | /** 67 | 自定义每个轮播图视图 68 | 69 | @param carousel 轮播图控件 70 | @param indexPath 轮播图cell实际下标 71 | @param index 业务逻辑需要的下标 72 | @return 自定义视图 73 | */ 74 | - (UICollectionViewCell *)viewForCarousel:(CWCarousel *)carousel indexPath:(NSIndexPath *)indexPath index:(NSInteger)index; 75 | @end 76 | 77 | 78 | @protocol CWCarouselPageControlProtocol 79 | @required 80 | /** 81 | 总页数 82 | */ 83 | @property (nonatomic, assign, readonly) NSInteger pageNumbers; 84 | /** 85 | 当前页 86 | */ 87 | @property (nonatomic, assign, readonly) NSInteger currentPage; 88 | 89 | - (void)setCurrentPage:(NSInteger)currentPage; 90 | - (void)setPageNumbers:(NSInteger)pageNumbers; 91 | @end 92 | #endif /* CWCarouselProtocol_h */ 93 | 94 | 95 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/code/CWPageControl.m: -------------------------------------------------------------------------------- 1 | // 2 | // CWPageControl.m 3 | // CWCarousel 4 | // 5 | // Created by chenwang on 2018/7/16. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import "CWPageControl.h" 10 | @interface CWPageControl () { 11 | 12 | } 13 | @property (nonatomic, assign) NSInteger myPageNumbers; 14 | @property (nonatomic, assign) NSInteger myCurrentPage; 15 | 16 | @property (nonatomic, strong) NSArray *indicatorArr; 17 | @property (nonatomic, strong) UIView *currentIndicator; 18 | @end 19 | @implementation CWPageControl 20 | @synthesize currentPage; 21 | @synthesize pageNumbers; 22 | 23 | #pragma mark - INITIAL 24 | - (instancetype)initWithFrame:(CGRect)frame { 25 | if(self = [super initWithFrame:frame]) { 26 | [self configureView]; 27 | } 28 | return self; 29 | } 30 | 31 | #pragma mark - PROPERTY 32 | - (void)setCurrentPage:(NSInteger)currentPage { 33 | if (currentPage >= self.indicatorArr.count) { 34 | return; 35 | } 36 | self.myCurrentPage = currentPage; 37 | UIView *indicator = self.indicatorArr[currentPage]; 38 | [UIView animateWithDuration:0.25 animations:^{ 39 | self.currentIndicator.frame = indicator.frame; 40 | }]; 41 | 42 | } 43 | 44 | - (void)setPageNumbers:(NSInteger)pageNumbers { 45 | if (self.pageNumbers == pageNumbers) { 46 | return; 47 | } 48 | self.myPageNumbers = pageNumbers; 49 | [self createIndicator]; 50 | } 51 | 52 | - (NSInteger)currentPage { 53 | return self.currentPage; 54 | } 55 | 56 | - (NSInteger)pageNumbers { 57 | return self.myPageNumbers; 58 | } 59 | 60 | #pragma mark - LOGIC HELPER 61 | - (void)configureView { 62 | 63 | } 64 | 65 | + (CGFloat)widthFromNumber:(NSInteger)num { 66 | return 25 * num + 2 * (num - 1); 67 | } 68 | 69 | - (void)createIndicator { 70 | [self.indicatorArr makeObjectsPerformSelector:@selector(removeFromSuperview)]; 71 | CGFloat width = 25; 72 | CGFloat height = 7; 73 | CGFloat space = 2; 74 | CGFloat containerWidth = (width + space) * self.pageNumbers - space; 75 | UIView *container = [[UIView alloc] initWithFrame:CGRectMake((CGRectGetWidth(self.frame) - containerWidth) * 0.5, 0, containerWidth, CGRectGetHeight(self.frame))]; 76 | [self addSubview:container]; 77 | NSMutableArray *arr = [NSMutableArray arrayWithCapacity:self.pageNumbers]; 78 | for(int i = 0; i < self.pageNumbers; i ++) { 79 | CGFloat y = (CGRectGetHeight(self.frame) - height) * 0.5; 80 | CGFloat x = (width + space) * i; 81 | UIView *indictor = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, height)]; 82 | [container addSubview:indictor]; 83 | indictor.backgroundColor = [UIColor groupTableViewBackgroundColor]; 84 | [arr addObject:indictor]; 85 | if(i == 0 && self.currentIndicator == nil) { 86 | UIView *indictor = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, height)]; 87 | [container addSubview:indictor]; 88 | self.currentIndicator = indictor; 89 | self.currentIndicator.backgroundColor = [UIColor redColor]; 90 | self.currentIndicator.layer.zPosition = 999; 91 | } 92 | } 93 | self.indicatorArr = [NSArray arrayWithArray:arr]; 94 | } 95 | @end 96 | -------------------------------------------------------------------------------- /OC_CWCarousel/CWCarousel.h: -------------------------------------------------------------------------------- 1 | // 2 | // CWCarousel.h 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CWCarouselProtocol.h" 11 | #import "CWFlowLayout.h" 12 | 13 | @class CWCarouselCollectionView; 14 | @interface CWCarousel : UIView 15 | #pragma mark - < 相关属性 > 16 | 17 | /** 18 | 控件版本号 19 | */ 20 | @property (nonatomic, readonly, copy) NSString * _Nullable version; 21 | 22 | 23 | /** 24 | 相关代理 25 | */ 26 | @property (nonatomic, assign) id _Nullable delegate; 27 | 28 | 29 | /** 30 | 相关数据源 31 | */ 32 | @property (nonatomic, assign) id _Nullable datasource; 33 | 34 | 35 | /** 36 | 布局自定义layout 37 | */ 38 | @property (nonatomic, strong, readonly) CWFlowLayout * _Nonnull flowLayout; 39 | 40 | 41 | /** 42 | 样式风格 43 | */ 44 | @property (nonatomic, assign, readonly) CWCarouselStyle style; 45 | 46 | 47 | /** 48 | style = CWCarouselStyle_H_3时的扩展高度 (1.1.0版本后该属性废弃,请不要使用了) 49 | */ 50 | @property (nonatomic, assign, readonly) CGFloat addHeight; 51 | 52 | 53 | /** 54 | 实际的示轮播图内容的视图(其实就是基于collectionView实现的) 55 | */ 56 | @property (nonatomic, strong, readonly) CWCarouselCollectionView * _Nonnull carouselView; 57 | 58 | 59 | /** 60 | 是否自动轮播, 默认为NO 61 | */ 62 | @property (nonatomic, assign) BOOL isAuto; 63 | 64 | 65 | /** 66 | 自动轮播时间间隔, 默认 3s 67 | */ 68 | @property (nonatomic, assign) NSTimeInterval autoTimInterval; 69 | 70 | 71 | /** 72 | 默认的pageControl, 当设置了customPageControl时, 该属性为nil 73 | */ 74 | @property (nonatomic, strong) UIPageControl * _Nullable pageControl; 75 | 76 | 77 | /** 78 | 自定义的pageControl 79 | */ 80 | @property (nonatomic, strong) UIView * _Nullable customPageControl; 81 | 82 | 83 | /** 84 | 是否开始无限轮播 85 | YES: 可以无限衔接 86 | NO: 滑动到第一张或者最后一张就不能滑动了 87 | */ 88 | @property (nonatomic, assign) BOOL endless; 89 | #pragma mark - < 相关方法 > 90 | /** 91 | 创建实例构造方法 92 | 93 | @param frame 尺寸大小 94 | @param delegate 代理 95 | @param datasource 数据源 96 | @param flowLayout 自定义flowlayout 97 | @return 实例对象 98 | */ 99 | - (instancetype _Nullable )initWithFrame:(CGRect)frame 100 | delegate:(id _Nullable)delegate 101 | datasource:(id _Nullable)datasource 102 | flowLayout:(nonnull CWFlowLayout *)flowLayout; 103 | 104 | /** 105 | 注册自定视图 106 | 107 | @param viewClass 自定义视图类名 108 | @param identifier 重用唯一标识符 109 | */ 110 | - (void)registerViewClass:(Class _Nullable )viewClass identifier:(NSString *_Nullable)identifier; 111 | 112 | /** 113 | 注册自定义视图 114 | 115 | @param nibName 自定义视图xib相关文件名 116 | @param identifier 重用唯一标识符 117 | */ 118 | - (void)registerNibView:(NSString *_Nullable)nibName identifier:(NSString *_Nullable)identifier; 119 | 120 | 121 | /** 122 | 刷新轮播图 123 | */ 124 | - (void)freshCarousel; 125 | 126 | /** 127 | 暂停轮播图后,可以调用改方法继续播放 128 | */ 129 | - (void)resumePlay; 130 | 131 | /** 132 | 轮播图暂停自动播放 133 | */ 134 | - (void)pause; 135 | 136 | /** 137 | 如果开启自动轮播,销毁前需要调用该方法,释放定时器.否则可能内存泄漏 138 | 注: 1.1.9版本之后无需再手动调用该方法, 控件内部会自动释放. 139 | */ 140 | - (void)releaseTimer; 141 | 142 | /** 143 | 轮播图所处控制器WillAppear方法里调用 144 | */ 145 | - (void)controllerWillAppear; 146 | 147 | /** 148 | 轮播图所处控制器WillDisAppear方法里调用 149 | */ 150 | - (void)controllerWillDisAppear; 151 | 152 | 153 | /// 滚动到指定下标 154 | /// @param index 指定下标 155 | /// @param animation 是否开启滚动动画 156 | - (void)scrollTo:(NSInteger)index animation:(BOOL)animation; 157 | @end 158 | 159 | 160 | 161 | 162 | 163 | 164 | @interface CWCarouselCollectionView: UICollectionView 165 | 166 | @end 167 | 168 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/TestController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestController.swift 3 | // CWCarousel 4 | // 5 | // Created by 陈旺 on 2021/10/8. 6 | // Copyright © 2021 ChenWang. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.view.backgroundColor = .cyan 16 | 17 | let width = self.view.frame.size.width 18 | let height: CGFloat = 400 19 | let scrollView = UIScrollView() 20 | scrollView.delegate = self 21 | scrollView.frame = CGRect(x: 0, y: 100, width: width, height: height) 22 | scrollView.backgroundColor = UIColor.white 23 | self.view.addSubview(scrollView) 24 | 25 | var view = UIView() 26 | view.frame = CGRect(x: 0, y: 0, width: width, height: height) 27 | view.backgroundColor = .red 28 | scrollView.addSubview(view) 29 | 30 | view = UIView() 31 | view.frame = CGRect(x: width, y: 0, width: width, height: height) 32 | view.backgroundColor = .orange 33 | scrollView.addSubview(view) 34 | 35 | scrollView.contentSize = CGSize(width: width * 2, height: height) 36 | if #available(iOS 11.0, *) { 37 | scrollView.contentInsetAdjustmentBehavior = .never 38 | } else { 39 | // Fallback on earlier versions 40 | } 41 | 42 | 43 | // let layout = UICollectionViewFlowLayout() 44 | // layout.itemSize = CGSize(width: self.view.frame.size.width, height: 400) 45 | // layout.minimumLineSpacing = 10 46 | // layout.scrollDirection = .horizontal 47 | // let collection = UICollectionView(frame: CGRect(x: 0, y: 100, width: self.view.frame.size.width, height: 400), 48 | // collectionViewLayout: layout) 49 | // collection.backgroundColor = .orange 50 | // self.view.addSubview(collection) 51 | // 52 | // collection.delegate = self 53 | // collection.dataSource = self 54 | // collection.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellId") 55 | } 56 | } 57 | 58 | extension TestController: UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource { 59 | 60 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 61 | return 5 62 | } 63 | 64 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 65 | let colors: [UIColor] = [.red, .orange, .yellow, .green, .blue] 66 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) 67 | cell.contentView.backgroundColor = colors[indexPath.row] 68 | return cell 69 | } 70 | 71 | 72 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 73 | 74 | } 75 | 76 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 77 | print("will begin dragging") 78 | } 79 | 80 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 81 | print("will end dragging") 82 | } 83 | 84 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 85 | print("did end dragging") 86 | } 87 | 88 | func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 89 | print("will begin decelerating") 90 | } 91 | 92 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 93 | print("did end decelerating") 94 | } 95 | 96 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 97 | print("did end scrolling animation") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/ShowViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowViewController.swift 3 | // CWCarousel 4 | // 5 | // Created by chenwang on 2018/7/20. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let cellReuseId = "cellReuseId" 12 | class ShowViewController: UIViewController { 13 | //MARK: - INITILAL 14 | init(style: CWBannerStyle) { 15 | self.bannerStyle = style 16 | super.init(nibName: nil, bundle: nil) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | self.bannerStyle = .unknown 21 | super.init(coder: aDecoder) 22 | } 23 | 24 | deinit { 25 | NSLog("[%@ -- %@]", NSStringFromClass(self.classForCoder), #function) 26 | } 27 | 28 | //MARK: - LIFE CYCLE 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | self.configureUI() 32 | } 33 | 34 | override func viewWillAppear(_ animated: Bool) { 35 | super.viewWillAppear(animated) 36 | self.bannerView.bannerWillAppear() 37 | } 38 | 39 | override func viewWillDisappear(_ animated: Bool) { 40 | super.viewWillDisappear(animated) 41 | self.bannerView.bannerWillDisAppear() 42 | } 43 | 44 | override func didReceiveMemoryWarning() { 45 | super.didReceiveMemoryWarning() 46 | } 47 | //MARK: - PROPERTY 48 | let bannerStyle: CWBannerStyle 49 | 50 | let imgNames = ["01.jpg", 51 | "02.jpg", 52 | "03.jpg", 53 | "04.jpg", 54 | "05.jpg"] 55 | 56 | lazy var bannerView: CWBanner = { 57 | let layout = CWSwiftFlowLayout.init(style: self.bannerStyle) 58 | layout.itemSpace = 10 59 | if self.bannerStyle == .preview_big { 60 | layout.itemSpace = -20 61 | } 62 | let banner = CWBanner.init(frame: CGRect.init(x: 0, y: 100, width: UIScreen.main.bounds.width, height: 240), flowLayout: layout, delegate: self) 63 | self.view.addSubview(banner) 64 | 65 | banner.backgroundColor = self.view.backgroundColor 66 | banner.banner.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellReuseId) 67 | 68 | banner.autoPlay = false 69 | banner.endless = true 70 | banner.timeInterval = 2 71 | 72 | return banner 73 | }() 74 | 75 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 76 | self.bannerView.scroll(to: 3, animated: false) 77 | } 78 | } 79 | 80 | //MARK: - CWBannerDelegate 81 | extension ShowViewController: CWBannerDelegate { 82 | func bannerNumbers() -> Int { 83 | return self.imgNames.count 84 | } 85 | 86 | func bannerView(banner: CWBanner, index: Int, indexPath: IndexPath) -> UICollectionViewCell { 87 | let cell = banner.banner.dequeueReusableCell(withReuseIdentifier: cellReuseId, for: indexPath) 88 | var imgView = cell.contentView.viewWithTag(999) 89 | var label = cell.contentView.viewWithTag(888) 90 | if imgView == nil { 91 | imgView = UIImageView.init(frame: cell.contentView.bounds) 92 | imgView?.tag = 999 93 | cell.contentView.addSubview(imgView!) 94 | imgView?.layer.cornerRadius = 4.0 95 | imgView?.layer.masksToBounds = true 96 | 97 | label = UILabel.init(frame: CGRect.init(x: 30, y: 0, width: 60, height: 30)) 98 | (label as! UILabel).textColor = UIColor.white 99 | (label as! UILabel).font = UIFont.systemFont(ofSize: 21) 100 | label?.tag = 888 101 | cell.contentView.addSubview(label!) 102 | } 103 | (imgView as! UIImageView).image = UIImage.init(named: self.imgNames[index]) 104 | (label as! UILabel).text = "\(index)" 105 | return cell 106 | } 107 | 108 | func didSelected(banner: CWBanner, index: Int, indexPath: IndexPath) { 109 | print("点击 \(index) click...") 110 | } 111 | 112 | func didStartScroll(banner: CWBanner, index: Int, indexPath: IndexPath) { 113 | print("开始滑动: \(index) ...") 114 | } 115 | 116 | func didEndScroll(banner: CWBanner, index: Int, indexPath: IndexPath) { 117 | print("结束滑动: \(index) ...") 118 | } 119 | } 120 | 121 | // MARK: - CONFIGURE UI 122 | extension ShowViewController { 123 | fileprivate func configureUI() { 124 | self.view.backgroundColor = UIColor.white 125 | self.navigationItem.title = "show" 126 | self.bannerView.freshBanner() 127 | self.automaticallyAdjustsScrollViewInsets = false 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CWCarousel 2 | 3 | ## 基于collectionView实现的通用无限轮播图封装. 4 | 5 | > 1. issues里面的可以重现的问题已经修复. 6 | > 2. 部分不能重现的问题暂未处理. 7 | > 3. 请大家提issue尽量描述详细些,最好能提供代码或者截图.请附上是OC版本还是Swift版本的问题. 8 | > 4. 谢谢配合,大家一起进步. 9 | > 5. 代码示例为oc代码, 项目中已有swift版本,请自行下载. 10 | > 6. oc版本支持cocoaPods安装 `pod search OC_CWCarousel` 11 | 12 | oc版和swift是两份完全不同的代码,所以代码实现不尽相同。有些可能在其中一个版本实现了,但是另一个版本不一定来得及实现。请参照下面的版本记录自行了解: 13 | 14 | #### OC:版本记录 15 | ![oc演示demo](https://github.com/baozoudiudiu/CWCarousel/blob/master/CWCarouselDemo/oc_demo.png?raw=true) 16 | 17 | | 版本号 | 更新内容 | 18 | | ------ | ------ | 19 | | 1.1.9 | 1、采用 NSTimer + NSProxy方式实现自动轮播,外部可以无需再手动调用releaseTimer方法来释放内存了。
2、优化了scrollToIndex内部实现
3、修改pageControl布局代理的触发逻辑 | 20 | | 1.1.8 | 1. 代码重构。
2. 计算间距算法修改,更好更简单的设置间距。
3. 新增代理回调:`- (void)CWCarousel:(CWCarousel*)carousel addPageControl:(UIView* )pageControl;`可以在该回调中自定义布局pageControl。
4. 新增滚动到指定位置方法:`- (void*scrollTo:(NSInteger)index animation:(BOOL)animation`;。
5. 废弃了`maxScale`属性。
6.优化了自定义pageControl的实现。
7.修改了演示demo首页样式和逻辑。 | 21 | | 1.1.7 | 适配iOS14 感谢[milletZZ](https://github.com/milletZZ) | 22 | | 1.1.5 | 1.新增了2个代理回调
2.点击事件代理回调逻辑优化 | 23 | | 1.1.4 | 修复样式3在320尺寸屏幕上可能出现的问题 | 24 | | 1.1.3 | 提升了一个版本号 | 25 | | 1.1.2 | 控件优化 | 26 | | 1.1.1 | 添加了无限轮播功能开关 endless | 27 | | 1.1.0 | 修复了之前的一些bug | 28 | 29 | #### swift:版本记录 30 | | 版本号 | 更新内容 | 31 | | ------ | :----- | 32 | | 1.1.9| 1、代码重构 2、timer实现方式优化,外部无需手动调用releaseTimer释放内存 3、计算间距算法修改,更好更简单的设置间距 4、新增滚动到指定位置方法| 33 | | 1.1.8| 1.fix bug,2. 自定义pageControl优化, 3.只有一张图时不可无限滑动, 4.无数据时可以设置占位视图| 34 | | 1.1.7 | 适配iOS14 感谢[milletZZ](https://github.com/milletZZ) | 35 | | 1.1.4 | 修复通过layout创建视图,偶现不自动轮播问题. | 36 | | 1.1.3 | 适配swift5.0 | 37 | | 1.1.2 | 修复样式3在320尺寸屏幕上可能出现的问题 | 38 | | 1.1.1 | 添加了无限轮播功能开关 endless | 39 | | 1.1.0 | 功能同OC版 1.1.0 | 40 | 41 | 42 | ![example.gif](https://upload-images.jianshu.io/upload_images/3096223-64b23965562677f7.gif?imageMogr2/auto-orient/strip) 43 | 44 | * 目前支持4种样式 45 | ``` 46 | typedef NS_ENUM(NSUInteger, CWCarouselStyle) { 47 | CWCarouselStyle_Unknow = 0, ///<未知样式 48 | CWCarouselStyle_Normal, ///<普通样式,一张图占用整个屏幕宽度 49 | CWCarouselStyle_H_1, ///<自定义样式一, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到 50 | CWCarouselStyle_H_2, ///<自定义样式二, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,并且中间一张图正常大小,前后2张图会缩放 51 | CWCarouselStyle_H_3, ///<自定义样式三, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,中间一张有放大效果,前后2张正常大小 52 | }; 53 | ``` 54 | > CWCarouselStyle_Normal 55 | 56 | 57 | ![normal.gif](https://upload-images.jianshu.io/upload_images/3096223-7a745a375cf86b75.gif?imageMogr2/auto-orient/strip) 58 | 59 | > CWCarouselStyle_H_1 60 | 61 | 62 | ![H_1.gif](https://upload-images.jianshu.io/upload_images/3096223-04925d699694000a.gif?imageMogr2/auto-orient/strip) 63 | > CWCarouselStyle_H_2 64 | 65 | 66 | ![H_2.gif](https://upload-images.jianshu.io/upload_images/3096223-158f78ab0329288e.gif?imageMogr2/auto-orient/strip) 67 | 68 | > CWCarouselStyle_H_3 69 | 70 | 71 | ![H_3.gif](https://upload-images.jianshu.io/upload_images/3096223-39307907361b1e4d.gif?imageMogr2/auto-orient/strip) 72 | 73 | * 控件实例对象创建 74 | 1. 创建flowLayout对象,设置轮播图风格 75 | ``` 76 | /** 77 | 构造方法 78 | 79 | @param style 轮播图风格 80 | @return 实例对象 81 | */ 82 | - (instancetype)initWithStyle:(CWCarouselStyle)style; 83 | 84 | // egg: 85 | CWFlowLayout *flowLayout = [[CWFlowLayout alloc] initWithStyle:[self styleFromTag:tag]]; 86 | ``` 87 | 2. 创建容器对象 88 | ``` 89 | /** 90 | 创建实例构造方法 91 | 92 | @param frame 尺寸大小 93 | @param delegate 代理 94 | @param datasource 数据源 95 | @param flowLayout 自定义flowlayout 96 | @return 实例对象 97 | */ 98 | - (instancetype _Nullable )initWithFrame:(CGRect)frame 99 | delegate:(id _Nullable)delegate 100 | datasource:(id _Nullable)datasource 101 | flowLayout:(nonnull CWFlowLayout *)flowLayout; 102 | 103 | // egg: 104 | CWCarousel *carousel = [[CWCarousel alloc] initWithFrame:self.animationView.bounds 105 | delegate:self 106 | datasource:self 107 | flowLayout:flowLayout]; 108 | ``` 109 | 3. 注册自定义cell,并实现代理方法,刷新视图 110 | ``` 111 | [carousel registerViewClass:[UICollectionViewCell class] identifier:@"cellId"]; 112 | [carousel freshCarousel]; 113 | 114 | #pragma mark - Delegate 115 | // 每个轮播图cell样式 116 | - (UICollectionViewCell *)viewForCarousel:(CWCarousel *)carousel indexPath:(NSIndexPath *)indexPath index:(NSInteger)index{ 117 | UICollectionViewCell *cell = [carousel.carouselView dequeueReusableCellWithReuseIdentifier:@"cellId" forIndexPath:indexPath]; 118 | /* 119 | your code 120 | */ 121 | return cell; 122 | } 123 | 124 | // 点击代理回调 125 | - (void)CWCarousel:(CWCarousel *)carousel didSelectedAtIndex:(NSInteger)index { 126 | NSLog(@"...%ld...", index); 127 | } 128 | 129 | // 轮播图个数 130 | - (NSInteger)numbersForCarousel { 131 | return kCount; 132 | } 133 | ``` 134 | 135 | 4. 为了流畅性和避免概率图片位置错乱问题,当开启自动滚动时,在banner所处的控制器生命周期中需要调用以下对应方法 136 | ``` 137 | /** 138 | 轮播图所处控制器WillAppear方法里调用 139 | */ 140 | - (void)controllerWillAppear; 141 | 142 | /** 143 | 轮播图所处控制器WillDisAppear方法里调用 144 | */ 145 | - (void)controllerWillDisAppear; 146 | ``` 147 | * 具体UI样式修改都有具体的属性,详情请查看对应类的.h文件即可. 148 | * 如有问题和bug,欢迎指正. 149 | 150 | -------------------------------------------------------------------------------- /Swift_CWCarousel/CWSwiftFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CWSwiftFlowLayout.swift 3 | // CWCarousel 4 | // 5 | // Created by chenwang on 2018/7/18. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// banner风格枚举 12 | enum CWBannerStyle { 13 | /// 未知样式 14 | case unknown 15 | /// 默认样式 16 | case normal 17 | /// 自定义样式一, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到 18 | case preview_normal 19 | /// 自定义样式二, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,并且中间一张图正常大小,前后2张图会缩放 20 | case preview_zoom 21 | /// 自定义样式三, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,中间一张有放大效果,前后2张正常大小 22 | case preview_big 23 | } 24 | 25 | class CWSwiftFlowLayout: UICollectionViewFlowLayout { 26 | //MARK: - 构造方法 27 | init(style: CWBannerStyle) { 28 | self.style = style 29 | super.init() 30 | } 31 | 32 | deinit { 33 | NSLog("[%@ -- %@]",NSStringFromClass(self.classForCoder) ,#function); 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | self.style = .unknown 38 | super.init(coder: aDecoder) 39 | } 40 | 41 | //MARK: - Override 42 | override func prepare() { 43 | super.prepare() 44 | guard self.collectionView != nil else { 45 | assert(self.collectionView != nil, "error") 46 | return 47 | } 48 | switch self.style { 49 | case .normal: 50 | self.initialNormalStyle() 51 | case .preview_normal: 52 | self.initialPreview_normalStyle() 53 | case .preview_zoom: 54 | self.initialPreview_zoomStyle() 55 | case .preview_big: 56 | self.initialPreview_bigStyle() 57 | default:() 58 | } 59 | } 60 | 61 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 62 | return true 63 | } 64 | 65 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 66 | var arr: [UICollectionViewLayoutAttributes]? = nil; 67 | switch self.style { 68 | case .unknown: fallthrough 69 | case .normal: fallthrough 70 | case .preview_normal: arr = super.layoutAttributesForElements(in: rect) 71 | case .preview_zoom: fallthrough 72 | case .preview_big: 73 | arr = self.caculateScale(rect: rect, block: { (width, space) -> CGFloat in 74 | return (self.minScale - 1) / (self.itemSize.width) * space + 1; 75 | }) 76 | } 77 | return arr 78 | } 79 | 80 | //MARK: - Property 81 | /// banner风格 82 | let style: CWBannerStyle 83 | 84 | /// 每张图之间的间距, 默认为0 85 | var itemSpace: CGFloat = 0 86 | 87 | /* 88 | * cell的宽度占总宽度的比例 89 | * 默认: 0.75 90 | * normal 样式下无效 91 | */ 92 | var itemWidthScale: CGFloat = 0.75 93 | 94 | /** 95 | * style = preview_zoom 有效 96 | * 前后2张图的缩小比例 (0.0 ~ 1.0) 97 | * 默认: 0.8 98 | */ 99 | var minScale: CGFloat = 0.8 100 | } 101 | 102 | 103 | // MARK: - Logic Helper 104 | extension CWSwiftFlowLayout { 105 | 106 | /// 计算cell缩放公式 107 | /// 108 | /// - Parameters: 109 | /// - rect: rect 110 | /// - block: 计算方法闭包 111 | /// - Returns: 112 | fileprivate func caculateScale(rect: CGRect, block: (CGFloat, CGFloat) -> CGFloat) -> [UICollectionViewLayoutAttributes]? { 113 | let arr = super.layoutAttributesForElements(in: rect)?.map({$0.copy()}) as! [UICollectionViewLayoutAttributes]? 114 | let centerX = self.collectionView!.contentOffset.x + self.collectionView!.frame.width * 0.5 115 | let width = self.collectionView!.frame.width * self.itemWidthScale 116 | var maxScale: CGFloat = 0 117 | var attri: UICollectionViewLayoutAttributes? = nil 118 | arr?.forEach({ (element) in 119 | let space = CGFloat(abs(element.center.x - centerX)) 120 | var scale: CGFloat = 1.0 121 | scale = block(width, space) 122 | element.transform = CGAffineTransform.init(scaleX: scale, y: scale) 123 | if maxScale < scale { 124 | maxScale = scale 125 | attri = element 126 | } 127 | element.zIndex = 0 128 | }) 129 | if attri != nil { 130 | attri?.zIndex = 1 131 | } 132 | return arr 133 | } 134 | 135 | /// 设置默认样式 136 | fileprivate func initialNormalStyle() { 137 | self.scrollDirection = .horizontal 138 | let width = self.collectionView!.frame.width 139 | let height = self.collectionView!.frame.height 140 | self.itemSize = CGSize.init(width: width, height: height) 141 | self.minimumLineSpacing = self.itemSpace; 142 | } 143 | 144 | /// 设置preview_默认样式 145 | fileprivate func initialPreview_normalStyle() { 146 | self.scrollDirection = .horizontal 147 | let height = self.collectionView!.frame.height 148 | let width = self.collectionView!.frame.width * self.itemWidthScale 149 | self.itemSize = CGSize.init(width: width, height: height) 150 | self.minimumLineSpacing = self.itemSpace; 151 | } 152 | 153 | /// 设置preiview_zoom样式 154 | fileprivate func initialPreview_zoomStyle() { 155 | self.scrollDirection = .horizontal 156 | let height = self.collectionView!.frame.height 157 | let width = self.collectionView!.frame.width * self.itemWidthScale 158 | self.itemSize = CGSize.init(width: width, height: height) 159 | let padding = width * (1 - self.minScale) * 0.5; 160 | self.minimumLineSpacing = self.itemSpace - padding; 161 | } 162 | 163 | /// 设置preiview_big样式 164 | fileprivate func initialPreview_bigStyle() { 165 | self.scrollDirection = .horizontal 166 | let height = self.collectionView!.frame.height 167 | let width = self.collectionView!.frame.width * self.itemWidthScale 168 | self.itemSize = CGSize.init(width: width, height: height) 169 | let padding = width * (1 - self.minScale) * 0.5; 170 | self.minimumLineSpacing = self.itemSpace - padding; 171 | } 172 | } 173 | 174 | -------------------------------------------------------------------------------- /OC_CWCarousel/CWFlowLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // CWFlowLayout.m 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import "CWFlowLayout.h" 10 | 11 | @interface CWFlowLayout () { 12 | 13 | } 14 | /** 15 | 默认轮播图宽度 16 | */ 17 | @property (nonatomic, assign) CGFloat defaultItemWidth; 18 | @property (nonatomic, assign) CGFloat factItemSpace; 19 | @end 20 | 21 | @implementation CWFlowLayout 22 | 23 | - (instancetype)initWithStyle:(CWCarouselStyle)style { 24 | if(self = [super init]) { 25 | self.style = style; 26 | [self initial]; 27 | } 28 | return self; 29 | } 30 | 31 | - (void)dealloc { 32 | NSLog(@"%s", __func__); 33 | } 34 | 35 | - (void)initial { 36 | self.itemSpace_H = 0; 37 | self.itemSpace_V = 0; 38 | self.minScale = 0.8; 39 | self.maxScale = 1.2; 40 | } 41 | 42 | - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { 43 | 44 | if (self.style == CWCarouselStyle_Unknow) { 45 | return NO; 46 | } 47 | 48 | if (self.style == CWCarouselStyle_Normal) { 49 | return NO; 50 | } 51 | 52 | if (self.style == CWCarouselStyle_H_1) { 53 | return NO; 54 | } 55 | 56 | return YES; 57 | } 58 | 59 | - (void)prepareLayout { 60 | [super prepareLayout]; 61 | switch (self.style) { 62 | case CWCarouselStyle_Normal: { 63 | CGFloat width = CGRectGetWidth(self.collectionView.frame); 64 | CGFloat height = CGRectGetHeight(self.collectionView.frame); 65 | self.itemWidth = width; 66 | self.itemSize = CGSizeMake(width, height); 67 | self.minimumLineSpacing = self.itemSpace_H; 68 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal; 69 | } 70 | break; 71 | case CWCarouselStyle_H_1: { 72 | CGFloat width = self.itemWidth <= 0 ? self.defaultItemWidth : self.itemWidth; 73 | self.itemWidth = width; 74 | CGFloat height = CGRectGetHeight(self.collectionView.frame); 75 | self.itemSize = CGSizeMake(width, height); 76 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal; 77 | self.minimumLineSpacing = self.itemSpace_H; 78 | break; 79 | } 80 | case CWCarouselStyle_H_2: { 81 | CGFloat width = self.itemWidth <= 0 ? self.defaultItemWidth : self.itemWidth; 82 | self.itemWidth = width; 83 | CGFloat height = CGRectGetHeight(self.collectionView.frame); 84 | self.itemSize = CGSizeMake(width, height); 85 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal; 86 | CGFloat padding = width * (1 - self.minScale) * 0.5; 87 | self.factItemSpace = 0; 88 | self.minimumLineSpacing = self.itemSpace_H - padding; 89 | } 90 | break; 91 | case CWCarouselStyle_H_3: { 92 | CGFloat width = self.itemWidth <= 0 ? self.defaultItemWidth : self.itemWidth; 93 | self.itemWidth = width; 94 | CGFloat height = CGRectGetHeight(self.collectionView.frame); 95 | self.itemSize = CGSizeMake(width, height); 96 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal; 97 | CGFloat padding = width * (1 - self.minScale) * 0.5; 98 | self.factItemSpace = 0; 99 | // if(width * (1 - self.minScale) * 0.5 < self.itemSpace_H) { 100 | // self.factItemSpace = self.itemSpace_H - width * (1 - self.minScale) * 0.5; 101 | // } 102 | // self.minimumLineSpacing = self.factItemSpace; 103 | self.minimumLineSpacing = self.itemSpace_H - padding; 104 | } 105 | break; 106 | 107 | default: 108 | break; 109 | } 110 | 111 | } 112 | 113 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { 114 | 115 | if (self.style == CWCarouselStyle_Unknow || 116 | self.style == CWCarouselStyle_Normal || 117 | self.style == CWCarouselStyle_H_1) { 118 | 119 | return [super layoutAttributesForElementsInRect:rect]; 120 | } 121 | 122 | NSArray *arr = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES]; 123 | 124 | CGFloat centerX = self.collectionView.contentOffset.x + CGRectGetWidth(self.collectionView.frame) * 0.5; 125 | __block CGFloat maxScale = 0; 126 | __block UICollectionViewLayoutAttributes *attri = nil; 127 | 128 | [arr enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 129 | CGFloat space = ABS(obj.center.x - centerX); 130 | space = MIN(space, self.itemWidth + self.factItemSpace); 131 | obj.zIndex = 0; 132 | if(space >= 0) { 133 | CGFloat scale = 1; 134 | if (self.style == CWCarouselStyle_H_2 || 135 | self.style == CWCarouselStyle_H_3) { 136 | /** 137 | 公式: scale = k * space + a 138 | 其中: k = (minScale - 1) / (itemWidth + factItemSpace) 139 | 其中: a = 1 140 | 综上所述: 141 | scale = (minScale - 1) / (itemWitdh + factItemSpace) * space + 1 142 | */ 143 | scale = (self.minScale - 1) / (self.itemWidth + self.factItemSpace) * space + 1; 144 | }else { 145 | // scale = -((self.maxScale - 1) / width) * space + self.maxScale; 146 | } 147 | obj.transform = CGAffineTransformMakeScale(scale, scale); 148 | if(maxScale < scale) { 149 | maxScale = scale; 150 | attri = obj; 151 | } 152 | } 153 | }]; 154 | 155 | if (attri) { 156 | attri.zIndex = 1; 157 | } 158 | 159 | return arr; 160 | } 161 | 162 | - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { 163 | return proposedContentOffset; 164 | } 165 | #pragma mark - Property 166 | - (CGFloat)defaultItemWidth { 167 | switch (self.style) { 168 | case CWCarouselStyle_Unknow: 169 | case CWCarouselStyle_Normal: 170 | return self.collectionView.frame.size.width; 171 | break; 172 | case CWCarouselStyle_H_1: 173 | case CWCarouselStyle_H_2: 174 | case CWCarouselStyle_H_3: 175 | return self.collectionView.frame.size.width * 0.75; 176 | break; 177 | default: 178 | break; 179 | } 180 | } 181 | 182 | - (void)setMaxScale:(CGFloat)maxScale { 183 | _maxScale = maxScale; 184 | if(maxScale < 1) { 185 | _maxScale = 1; 186 | } 187 | } 188 | 189 | - (void)setMinScale:(CGFloat)minScale { 190 | _minScale = minScale; 191 | if(minScale < 0) { 192 | _minScale = 0.1; 193 | } 194 | if (minScale >= 1) { 195 | _minScale = 1; 196 | } 197 | } 198 | @end 199 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "CWCarousel.h" 11 | #import "CWPageControl.h" 12 | 13 | #define kViewTag 666 14 | #define kCount 0 15 | 16 | @interface ViewController () 17 | 18 | @property (nonatomic, strong) CWCarousel *carousel; 19 | @property (nonatomic, assign) BOOL openCustomPageControl; 20 | @property (weak, nonatomic) IBOutlet UIView *contentView; 21 | @property (nonatomic, strong) NSArray *dataArr; 22 | @property (weak, nonatomic) IBOutlet UISegmentedControl *styleSegment; 23 | @property (weak, nonatomic) IBOutlet UISwitch *endlessSwitch; 24 | @property (weak, nonatomic) IBOutlet UISwitch *autoSwitch; 25 | @property (weak, nonatomic) IBOutlet UILabel *spaceLab; 26 | @property (weak, nonatomic) IBOutlet UIStepper *spaceSteper; 27 | @property (weak, nonatomic) IBOutlet UISwitch *cusPageControlSwitch; 28 | @end 29 | 30 | @implementation ViewController 31 | - (void)dealloc { 32 | NSLog(@"%s", __func__); 33 | } 34 | 35 | - (void)viewDidLoad { 36 | [super viewDidLoad]; 37 | self.navigationItem.title = @"demo"; 38 | [self configureUI:0]; 39 | } 40 | 41 | - (void)configureUI:(NSInteger)tag { 42 | self.dataArr = nil; 43 | CATransition *tr = [CATransition animation]; 44 | tr.type = @"cube"; 45 | tr.subtype = kCATransitionFromRight; 46 | tr.duration = 0.25; 47 | [self.contentView.layer addAnimation:tr forKey:nil]; 48 | 49 | self.view.backgroundColor = [UIColor whiteColor]; 50 | if(self.carousel) { 51 | [self.carousel removeFromSuperview]; 52 | self.carousel = nil; 53 | } 54 | 55 | CWFlowLayout *flowLayout = [[CWFlowLayout alloc] initWithStyle:[self styleFromTag:tag]]; 56 | flowLayout.itemSpace_H = 0; 57 | self.spaceLab.text = @"0"; 58 | self.spaceSteper.value = 0; 59 | if (flowLayout.style == CWCarouselStyle_H_3) { 60 | flowLayout.itemSpace_H = -10; 61 | self.spaceLab.text = @"-10"; 62 | self.spaceSteper.value = -10; 63 | } 64 | // 使用layout创建视图(使用masonry 或者 系统api) 65 | CWCarousel *carousel = [[CWCarousel alloc] initWithFrame:CGRectZero 66 | delegate:self 67 | datasource:self 68 | flowLayout:flowLayout]; 69 | self.carousel = carousel; 70 | if (self.cusPageControlSwitch.isOn) { 71 | [self setPageControl]; 72 | } 73 | carousel.translatesAutoresizingMaskIntoConstraints = NO; 74 | [self.contentView addSubview:carousel]; 75 | NSDictionary *dic = @{@"view" : carousel}; 76 | [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[view]-0-|" 77 | options:kNilOptions 78 | metrics:nil 79 | views:dic]]; 80 | [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view]-0-|" 81 | options:kNilOptions 82 | metrics:nil 83 | views:dic]]; 84 | carousel.isAuto = self.autoSwitch.isOn; 85 | carousel.autoTimInterval = 2; 86 | carousel.endless = self.endlessSwitch.isOn; 87 | carousel.backgroundColor = [UIColor whiteColor]; 88 | [carousel registerViewClass:[UICollectionViewCell class] identifier:@"cellId"]; 89 | [self requestNetworkData]; 90 | } 91 | 92 | - (void)viewWillAppear:(BOOL)animated { 93 | [super viewWillAppear:animated]; 94 | [self.carousel controllerWillAppear]; 95 | } 96 | 97 | - (void)viewWillDisappear:(BOOL)animated { 98 | [super viewWillDisappear:animated]; 99 | [self.carousel controllerWillDisAppear]; 100 | } 101 | 102 | 103 | #pragma mark - < 事件响应 > 104 | - (CWCarouselStyle)styleFromTag:(NSInteger)tag { 105 | switch (tag) { 106 | case 0: 107 | return CWCarouselStyle_Normal; 108 | break; 109 | case 1: 110 | return CWCarouselStyle_H_1; 111 | break; 112 | case 2: 113 | return CWCarouselStyle_H_2; 114 | break; 115 | case 3: 116 | return CWCarouselStyle_H_3; 117 | break; 118 | default: 119 | return CWCarouselStyle_Unknow; 120 | break; 121 | } 122 | } 123 | 124 | - (IBAction)styleChanged:(UISegmentedControl *)sender { 125 | [self configureUI:sender.selectedSegmentIndex]; 126 | } 127 | 128 | - (IBAction)endlessChanged:(UISwitch *)sender { 129 | self.carousel.endless = sender.isOn; 130 | [self.carousel freshCarousel]; 131 | } 132 | 133 | - (IBAction)autoChanged:(UISwitch *)sender { 134 | self.carousel.isAuto = sender.isOn; 135 | [self.carousel freshCarousel]; 136 | } 137 | 138 | - (IBAction)spaceChanged:(UIStepper *)sender { 139 | self.carousel.flowLayout.itemSpace_H = sender.value; 140 | self.spaceLab.text = [NSString stringWithFormat:@"%.0f", sender.value]; 141 | [self.carousel freshCarousel]; 142 | } 143 | 144 | - (IBAction)customPageControlChanged:(UISwitch *)sender { 145 | if (sender.isOn) { 146 | [self setPageControl]; 147 | }else { 148 | self.carousel.customPageControl = nil; 149 | } 150 | [self.carousel freshCarousel]; 151 | } 152 | 153 | - (void)setPageControl { 154 | CGFloat width = [CWPageControl widthFromNumber:self.dataArr.count]; 155 | CWPageControl *pageC = [[CWPageControl alloc] initWithFrame:CGRectMake(0, 0, width, 20)]; 156 | pageC.translatesAutoresizingMaskIntoConstraints = NO; 157 | [[pageC.widthAnchor constraintEqualToConstant:width] setActive:YES]; 158 | /** 159 | 这里我自己给了宽度, 所以就不再自定义布局代理: 160 | - (void)CWCarousel:(CWCarousel *)carousel addPageControl:(UIView *)pageControl; 161 | */ 162 | self.carousel.customPageControl = pageC; 163 | } 164 | 165 | - (IBAction)buttonClick { 166 | [self.carousel scrollTo:2 animation:YES]; 167 | } 168 | 169 | #pragma mark - 网络层 170 | - (void)requestNetworkData { 171 | // 模拟网络请求 172 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 173 | NSMutableArray *arr = [NSMutableArray array]; 174 | NSString *imgName = @""; 175 | for (int i = 0; i < 5; i++) { 176 | imgName = [NSString stringWithFormat:@"%02d.jpg", i + 1]; 177 | [arr addObject:imgName]; 178 | } 179 | self.dataArr = arr; 180 | dispatch_async(dispatch_get_main_queue(), ^{ 181 | [self.carousel freshCarousel]; 182 | }); 183 | }); 184 | } 185 | 186 | #pragma mark - Delegate 187 | - (NSInteger)numbersForCarousel { 188 | return self.dataArr.count; 189 | } 190 | 191 | - (UICollectionViewCell *)viewForCarousel:(CWCarousel *)carousel indexPath:(NSIndexPath *)indexPath index:(NSInteger)index{ 192 | UICollectionViewCell *cell = [carousel.carouselView dequeueReusableCellWithReuseIdentifier:@"cellId" forIndexPath:indexPath]; 193 | cell.contentView.backgroundColor = [UIColor cyanColor]; 194 | UIImageView *imgView = [cell.contentView viewWithTag:kViewTag]; 195 | if(!imgView) { 196 | imgView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds]; 197 | imgView.tag = kViewTag; 198 | imgView.backgroundColor = [UIColor redColor]; 199 | imgView.contentMode = UIViewContentModeScaleAspectFill; 200 | [cell.contentView addSubview:imgView]; 201 | cell.layer.masksToBounds = YES; 202 | cell.layer.cornerRadius = 8; 203 | } 204 | // https://www.google.com/url?sa=i&rct=j&q=&esrc=s&source=images&cd=&cad=rja&uact=8&ved=2ahUKEwio8MyTp-DdAhWKM94KHUmEDcAQjRx6BAgBEAU&url=http%3A%2F%2F699pic.com%2Ftupian%2Fchuan.html&psig=AOvVaw20gpsPpW4JcNm0mJi9dYrb&ust=1538313533814128 205 | 206 | NSString *name = self.dataArr[index]; 207 | UIImage *img = [UIImage imageNamed:name]; 208 | if(!img) { 209 | NSLog(@"%@", name); 210 | } 211 | [imgView setImage:img]; 212 | return cell; 213 | } 214 | 215 | - (void)CWCarousel:(CWCarousel *)carousel didSelectedAtIndex:(NSInteger)index { 216 | NSLog(@"did selected at index %ld", index); 217 | } 218 | 219 | 220 | - (void)CWCarousel:(CWCarousel *)carousel didStartScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow { 221 | 222 | } 223 | 224 | 225 | - (void)CWCarousel:(CWCarousel *)carousel didEndScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow { 226 | 227 | } 228 | 229 | 230 | #pragma mark - Setter && Getter 231 | - (void)didReceiveMemoryWarning { 232 | [super didReceiveMemoryWarning]; 233 | } 234 | @end 235 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2826F4472101C3EE00BC32CB /* ShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2826F4462101C3EE00BC32CB /* ShowViewController.swift */; }; 11 | 2882BCDF20FC7B97006E8063 /* CWPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 2882BCDE20FC7B97006E8063 /* CWPageControl.m */; }; 12 | 28D8861920F44EAE00BDED3D /* 05.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861420F44EAD00BDED3D /* 05.jpg */; }; 13 | 28D8861A20F44EAE00BDED3D /* 03.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861520F44EAD00BDED3D /* 03.jpg */; }; 14 | 28D8861B20F44EAE00BDED3D /* 04.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861620F44EAE00BDED3D /* 04.jpg */; }; 15 | 28D8861C20F44EAE00BDED3D /* 02.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861720F44EAE00BDED3D /* 02.jpg */; }; 16 | 28D8861D20F44EAE00BDED3D /* 01.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861820F44EAE00BDED3D /* 01.jpg */; }; 17 | 6647FD98258E079A005A9547 /* CWBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6647FD96258E079A005A9547 /* CWBanner.swift */; }; 18 | 6647FD99258E079A005A9547 /* CWSwiftFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6647FD97258E079A005A9547 /* CWSwiftFlowLayout.swift */; }; 19 | 6648C317272A719B00DD47AF /* CWProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6648C316272A719B00DD47AF /* CWProxy.swift */; }; 20 | 665B49FF271028C8006872EA /* TestController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665B49FE271028C8006872EA /* TestController.swift */; }; 21 | 666851A22073BBF900F91985 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 666851A12073BBF900F91985 /* AppDelegate.m */; }; 22 | 666851A52073BBF900F91985 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 666851A42073BBF900F91985 /* ViewController.m */; }; 23 | 666851A82073BBF900F91985 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666851A62073BBF900F91985 /* Main.storyboard */; }; 24 | 666851AA2073BBF900F91985 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666851A92073BBF900F91985 /* Assets.xcassets */; }; 25 | 666851AD2073BBF900F91985 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666851AB2073BBF900F91985 /* LaunchScreen.storyboard */; }; 26 | 666851B02073BBF900F91985 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 666851AF2073BBF900F91985 /* main.m */; }; 27 | 666851BA2073BC8B00F91985 /* SViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666851B92073BC8B00F91985 /* SViewController.swift */; }; 28 | 6680CED625A3368000707014 /* CWFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6680CED425A3368000707014 /* CWFlowLayout.m */; }; 29 | 6680CED725A3368000707014 /* CWCarousel.m in Sources */ = {isa = PBXBuildFile; fileRef = 6680CED525A3368000707014 /* CWCarousel.m */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 2826F4462101C3EE00BC32CB /* ShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowViewController.swift; sourceTree = ""; }; 34 | 2882BCDD20FC7B97006E8063 /* CWPageControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CWPageControl.h; sourceTree = ""; }; 35 | 2882BCDE20FC7B97006E8063 /* CWPageControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CWPageControl.m; sourceTree = ""; }; 36 | 28D8861420F44EAD00BDED3D /* 05.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 05.jpg; sourceTree = ""; }; 37 | 28D8861520F44EAD00BDED3D /* 03.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 03.jpg; sourceTree = ""; }; 38 | 28D8861620F44EAE00BDED3D /* 04.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 04.jpg; sourceTree = ""; }; 39 | 28D8861720F44EAE00BDED3D /* 02.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 02.jpg; sourceTree = ""; }; 40 | 28D8861820F44EAE00BDED3D /* 01.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 01.jpg; sourceTree = ""; }; 41 | 6647FD96258E079A005A9547 /* CWBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CWBanner.swift; path = ../../../Swift_CWCarousel/CWBanner.swift; sourceTree = ""; }; 42 | 6647FD97258E079A005A9547 /* CWSwiftFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CWSwiftFlowLayout.swift; path = ../../../Swift_CWCarousel/CWSwiftFlowLayout.swift; sourceTree = ""; }; 43 | 6648C316272A719B00DD47AF /* CWProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CWProxy.swift; sourceTree = ""; }; 44 | 665B49FE271028C8006872EA /* TestController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestController.swift; sourceTree = ""; }; 45 | 6668519D2073BBF800F91985 /* CWCarousel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CWCarousel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 666851A02073BBF900F91985 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 47 | 666851A12073BBF900F91985 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 48 | 666851A32073BBF900F91985 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 49 | 666851A42073BBF900F91985 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 50 | 666851A72073BBF900F91985 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 51 | 666851A92073BBF900F91985 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52 | 666851AC2073BBF900F91985 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 53 | 666851AE2073BBF900F91985 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | 666851AF2073BBF900F91985 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 666851B82073BC8A00F91985 /* CWCarousel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CWCarousel-Bridging-Header.h"; sourceTree = ""; }; 56 | 666851B92073BC8B00F91985 /* SViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SViewController.swift; sourceTree = ""; }; 57 | 6680CED025A3368000707014 /* CWCarouselHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWCarouselHeader.h; path = ../../../OC_CWCarousel/CWCarouselHeader.h; sourceTree = ""; }; 58 | 6680CED125A3368000707014 /* CWCarousel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWCarousel.h; path = ../../../OC_CWCarousel/CWCarousel.h; sourceTree = ""; }; 59 | 6680CED225A3368000707014 /* CWCarouselProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWCarouselProtocol.h; path = ../../../OC_CWCarousel/CWCarouselProtocol.h; sourceTree = ""; }; 60 | 6680CED325A3368000707014 /* CWFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWFlowLayout.h; path = ../../../OC_CWCarousel/CWFlowLayout.h; sourceTree = ""; }; 61 | 6680CED425A3368000707014 /* CWFlowLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CWFlowLayout.m; path = ../../../OC_CWCarousel/CWFlowLayout.m; sourceTree = ""; }; 62 | 6680CED525A3368000707014 /* CWCarousel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CWCarousel.m; path = ../../../OC_CWCarousel/CWCarousel.m; sourceTree = ""; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | 6668519A2073BBF800F91985 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | /* End PBXFrameworksBuildPhase section */ 74 | 75 | /* Begin PBXGroup section */ 76 | 281EFA0D20FF39AD001859F3 /* Viewcontrollers */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 666851A32073BBF900F91985 /* ViewController.h */, 80 | 666851A42073BBF900F91985 /* ViewController.m */, 81 | 666851B92073BC8B00F91985 /* SViewController.swift */, 82 | 2826F4462101C3EE00BC32CB /* ShowViewController.swift */, 83 | 665B49FE271028C8006872EA /* TestController.swift */, 84 | ); 85 | name = Viewcontrollers; 86 | sourceTree = ""; 87 | }; 88 | 2882BCDC20FC7B74006E8063 /* code */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 2882BCDD20FC7B97006E8063 /* CWPageControl.h */, 92 | 2882BCDE20FC7B97006E8063 /* CWPageControl.m */, 93 | ); 94 | path = code; 95 | sourceTree = ""; 96 | }; 97 | 666851942073BBF800F91985 = { 98 | isa = PBXGroup; 99 | children = ( 100 | 6668519F2073BBF800F91985 /* CWCarousel */, 101 | 6668519E2073BBF800F91985 /* Products */, 102 | ); 103 | sourceTree = ""; 104 | }; 105 | 6668519E2073BBF800F91985 /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 6668519D2073BBF800F91985 /* CWCarousel.app */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 6668519F2073BBF800F91985 /* CWCarousel */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 666851A02073BBF900F91985 /* AppDelegate.h */, 117 | 666851A12073BBF900F91985 /* AppDelegate.m */, 118 | 281EFA0D20FF39AD001859F3 /* Viewcontrollers */, 119 | 2882BCDC20FC7B74006E8063 /* code */, 120 | 66704EA7209B427300286A9B /* Sources */, 121 | 666851B72073BC0C00F91985 /* Swift */, 122 | 666851B62073BBFE00F91985 /* OC */, 123 | ); 124 | path = CWCarousel; 125 | sourceTree = ""; 126 | }; 127 | 666851B62073BBFE00F91985 /* OC */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 6680CED125A3368000707014 /* CWCarousel.h */, 131 | 6680CED525A3368000707014 /* CWCarousel.m */, 132 | 6680CED025A3368000707014 /* CWCarouselHeader.h */, 133 | 6680CED225A3368000707014 /* CWCarouselProtocol.h */, 134 | 6680CED325A3368000707014 /* CWFlowLayout.h */, 135 | 6680CED425A3368000707014 /* CWFlowLayout.m */, 136 | ); 137 | path = OC; 138 | sourceTree = ""; 139 | }; 140 | 666851B72073BC0C00F91985 /* Swift */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 6648C316272A719B00DD47AF /* CWProxy.swift */, 144 | 6647FD96258E079A005A9547 /* CWBanner.swift */, 145 | 6647FD97258E079A005A9547 /* CWSwiftFlowLayout.swift */, 146 | 666851B82073BC8A00F91985 /* CWCarousel-Bridging-Header.h */, 147 | ); 148 | path = Swift; 149 | sourceTree = ""; 150 | }; 151 | 66704EA7209B427300286A9B /* Sources */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 666851A62073BBF900F91985 /* Main.storyboard */, 155 | 666851A92073BBF900F91985 /* Assets.xcassets */, 156 | 666851AB2073BBF900F91985 /* LaunchScreen.storyboard */, 157 | 666851AE2073BBF900F91985 /* Info.plist */, 158 | 666851AF2073BBF900F91985 /* main.m */, 159 | 28D8861820F44EAE00BDED3D /* 01.jpg */, 160 | 28D8861720F44EAE00BDED3D /* 02.jpg */, 161 | 28D8861520F44EAD00BDED3D /* 03.jpg */, 162 | 28D8861620F44EAE00BDED3D /* 04.jpg */, 163 | 28D8861420F44EAD00BDED3D /* 05.jpg */, 164 | ); 165 | path = Sources; 166 | sourceTree = ""; 167 | }; 168 | /* End PBXGroup section */ 169 | 170 | /* Begin PBXNativeTarget section */ 171 | 6668519C2073BBF800F91985 /* CWCarousel */ = { 172 | isa = PBXNativeTarget; 173 | buildConfigurationList = 666851B32073BBF900F91985 /* Build configuration list for PBXNativeTarget "CWCarousel" */; 174 | buildPhases = ( 175 | 666851992073BBF800F91985 /* Sources */, 176 | 6668519A2073BBF800F91985 /* Frameworks */, 177 | 6668519B2073BBF800F91985 /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | ); 183 | name = CWCarousel; 184 | productName = CWCarousel; 185 | productReference = 6668519D2073BBF800F91985 /* CWCarousel.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 666851952073BBF800F91985 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastUpgradeCheck = 0940; 195 | ORGANIZATIONNAME = ChenWang; 196 | TargetAttributes = { 197 | 6668519C2073BBF800F91985 = { 198 | CreatedOnToolsVersion = 9.0; 199 | LastSwiftMigration = 0900; 200 | ProvisioningStyle = Automatic; 201 | }; 202 | }; 203 | }; 204 | buildConfigurationList = 666851982073BBF800F91985 /* Build configuration list for PBXProject "CWCarousel" */; 205 | compatibilityVersion = "Xcode 8.0"; 206 | developmentRegion = en; 207 | hasScannedForEncodings = 0; 208 | knownRegions = ( 209 | en, 210 | Base, 211 | ); 212 | mainGroup = 666851942073BBF800F91985; 213 | productRefGroup = 6668519E2073BBF800F91985 /* Products */; 214 | projectDirPath = ""; 215 | projectRoot = ""; 216 | targets = ( 217 | 6668519C2073BBF800F91985 /* CWCarousel */, 218 | ); 219 | }; 220 | /* End PBXProject section */ 221 | 222 | /* Begin PBXResourcesBuildPhase section */ 223 | 6668519B2073BBF800F91985 /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 28D8861A20F44EAE00BDED3D /* 03.jpg in Resources */, 228 | 666851AD2073BBF900F91985 /* LaunchScreen.storyboard in Resources */, 229 | 666851AA2073BBF900F91985 /* Assets.xcassets in Resources */, 230 | 28D8861C20F44EAE00BDED3D /* 02.jpg in Resources */, 231 | 28D8861920F44EAE00BDED3D /* 05.jpg in Resources */, 232 | 666851A82073BBF900F91985 /* Main.storyboard in Resources */, 233 | 28D8861D20F44EAE00BDED3D /* 01.jpg in Resources */, 234 | 28D8861B20F44EAE00BDED3D /* 04.jpg in Resources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | /* End PBXResourcesBuildPhase section */ 239 | 240 | /* Begin PBXSourcesBuildPhase section */ 241 | 666851992073BBF800F91985 /* Sources */ = { 242 | isa = PBXSourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | 6648C317272A719B00DD47AF /* CWProxy.swift in Sources */, 246 | 666851A52073BBF900F91985 /* ViewController.m in Sources */, 247 | 6680CED625A3368000707014 /* CWFlowLayout.m in Sources */, 248 | 666851B02073BBF900F91985 /* main.m in Sources */, 249 | 665B49FF271028C8006872EA /* TestController.swift in Sources */, 250 | 6647FD99258E079A005A9547 /* CWSwiftFlowLayout.swift in Sources */, 251 | 666851BA2073BC8B00F91985 /* SViewController.swift in Sources */, 252 | 2826F4472101C3EE00BC32CB /* ShowViewController.swift in Sources */, 253 | 2882BCDF20FC7B97006E8063 /* CWPageControl.m in Sources */, 254 | 666851A22073BBF900F91985 /* AppDelegate.m in Sources */, 255 | 6647FD98258E079A005A9547 /* CWBanner.swift in Sources */, 256 | 6680CED725A3368000707014 /* CWCarousel.m in Sources */, 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | /* End PBXSourcesBuildPhase section */ 261 | 262 | /* Begin PBXVariantGroup section */ 263 | 666851A62073BBF900F91985 /* Main.storyboard */ = { 264 | isa = PBXVariantGroup; 265 | children = ( 266 | 666851A72073BBF900F91985 /* Base */, 267 | ); 268 | name = Main.storyboard; 269 | sourceTree = ""; 270 | }; 271 | 666851AB2073BBF900F91985 /* LaunchScreen.storyboard */ = { 272 | isa = PBXVariantGroup; 273 | children = ( 274 | 666851AC2073BBF900F91985 /* Base */, 275 | ); 276 | name = LaunchScreen.storyboard; 277 | sourceTree = ""; 278 | }; 279 | /* End PBXVariantGroup section */ 280 | 281 | /* Begin XCBuildConfiguration section */ 282 | 666851B12073BBF900F91985 /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 289 | CLANG_CXX_LIBRARY = "libc++"; 290 | CLANG_ENABLE_MODULES = YES; 291 | CLANG_ENABLE_OBJC_ARC = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 308 | CLANG_WARN_STRICT_PROTOTYPES = YES; 309 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 310 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 311 | CLANG_WARN_UNREACHABLE_CODE = YES; 312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 313 | CODE_SIGN_IDENTITY = "iPhone Developer"; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = dwarf; 316 | ENABLE_STRICT_OBJC_MSGSEND = YES; 317 | ENABLE_TESTABILITY = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_DYNAMIC_NO_PIC = NO; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_OPTIMIZATION_LEVEL = 0; 322 | GCC_PREPROCESSOR_DEFINITIONS = ( 323 | "DEBUG=1", 324 | "$(inherited)", 325 | ); 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 333 | MTL_ENABLE_DEBUG_INFO = YES; 334 | ONLY_ACTIVE_ARCH = YES; 335 | SDKROOT = iphoneos; 336 | }; 337 | name = Debug; 338 | }; 339 | 666851B22073BBF900F91985 /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 345 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 346 | CLANG_CXX_LIBRARY = "libc++"; 347 | CLANG_ENABLE_MODULES = YES; 348 | CLANG_ENABLE_OBJC_ARC = YES; 349 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 350 | CLANG_WARN_BOOL_CONVERSION = YES; 351 | CLANG_WARN_COMMA = YES; 352 | CLANG_WARN_CONSTANT_CONVERSION = YES; 353 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 354 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 355 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 356 | CLANG_WARN_EMPTY_BODY = YES; 357 | CLANG_WARN_ENUM_CONVERSION = YES; 358 | CLANG_WARN_INFINITE_RECURSION = YES; 359 | CLANG_WARN_INT_CONVERSION = YES; 360 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 361 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 362 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 364 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 365 | CLANG_WARN_STRICT_PROTOTYPES = YES; 366 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 367 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 368 | CLANG_WARN_UNREACHABLE_CODE = YES; 369 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 370 | CODE_SIGN_IDENTITY = "iPhone Developer"; 371 | COPY_PHASE_STRIP = NO; 372 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu11; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 378 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 379 | GCC_WARN_UNDECLARED_SELECTOR = YES; 380 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 381 | GCC_WARN_UNUSED_FUNCTION = YES; 382 | GCC_WARN_UNUSED_VARIABLE = YES; 383 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 384 | MTL_ENABLE_DEBUG_INFO = NO; 385 | SDKROOT = iphoneos; 386 | SWIFT_COMPILATION_MODE = wholemodule; 387 | VALIDATE_PRODUCT = YES; 388 | }; 389 | name = Release; 390 | }; 391 | 666851B42073BBF900F91985 /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | CLANG_ENABLE_MODULES = YES; 396 | CODE_SIGN_STYLE = Automatic; 397 | CURRENT_PROJECT_VERSION = 1.1.8; 398 | DEVELOPMENT_TEAM = V3DM7W7CZT; 399 | INFOPLIST_FILE = "$(SRCROOT)/CWCarousel/Sources/Info.plist"; 400 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 401 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 402 | MARKETING_VERSION = 1.1.8; 403 | PRODUCT_BUNDLE_IDENTIFIER = "com.chenwang.CWCarousel-01"; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | SWIFT_OBJC_BRIDGING_HEADER = "CWCarousel/Swift/CWCarousel-Bridging-Header.h"; 406 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 407 | SWIFT_VERSION = 5.0; 408 | TARGETED_DEVICE_FAMILY = "1,2"; 409 | }; 410 | name = Debug; 411 | }; 412 | 666851B52073BBF900F91985 /* Release */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 416 | CLANG_ENABLE_MODULES = YES; 417 | CODE_SIGN_STYLE = Automatic; 418 | CURRENT_PROJECT_VERSION = 1.1.8; 419 | DEVELOPMENT_TEAM = V3DM7W7CZT; 420 | INFOPLIST_FILE = "$(SRCROOT)/CWCarousel/Sources/Info.plist"; 421 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 422 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 423 | MARKETING_VERSION = 1.1.8; 424 | PRODUCT_BUNDLE_IDENTIFIER = "com.chenwang.CWCarousel-01"; 425 | PRODUCT_NAME = "$(TARGET_NAME)"; 426 | SWIFT_OBJC_BRIDGING_HEADER = "CWCarousel/Swift/CWCarousel-Bridging-Header.h"; 427 | SWIFT_VERSION = 5.0; 428 | TARGETED_DEVICE_FAMILY = "1,2"; 429 | }; 430 | name = Release; 431 | }; 432 | /* End XCBuildConfiguration section */ 433 | 434 | /* Begin XCConfigurationList section */ 435 | 666851982073BBF800F91985 /* Build configuration list for PBXProject "CWCarousel" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | 666851B12073BBF900F91985 /* Debug */, 439 | 666851B22073BBF900F91985 /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | 666851B32073BBF900F91985 /* Build configuration list for PBXNativeTarget "CWCarousel" */ = { 445 | isa = XCConfigurationList; 446 | buildConfigurations = ( 447 | 666851B42073BBF900F91985 /* Debug */, 448 | 666851B52073BBF900F91985 /* Release */, 449 | ); 450 | defaultConfigurationIsVisible = 0; 451 | defaultConfigurationName = Release; 452 | }; 453 | /* End XCConfigurationList section */ 454 | }; 455 | rootObject = 666851952073BBF800F91985 /* Project object */; 456 | } 457 | -------------------------------------------------------------------------------- /Swift_CWCarousel/CWBanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CWBanner.swift 3 | // CWCarousel 4 | // 5 | // Created by chenwang on 2018/7/18. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CWCollectionView: UICollectionView, UIGestureRecognizerDelegate { 12 | 13 | override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 14 | super.init(frame: frame, collectionViewLayout: layout) 15 | self.panGestureRecognizer.delegate = self 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 23 | return true 24 | } 25 | } 26 | 27 | protocol CWBannerDelegate: AnyObject { 28 | /// 视图数量 29 | func bannerNumbers() -> Int 30 | 31 | /// 加载cell 32 | /// - Parameters: 33 | /// - banner: 控件对象 34 | /// - index: 当前需要加载index 35 | /// - indexPath: 当前需要加载indexPath 36 | func bannerView(banner: CWBanner, index: Int, indexPath: IndexPath) -> UICollectionViewCell 37 | 38 | /// 点击 39 | /// - Parameters: 40 | /// - banner: 控件对象 41 | /// - index: 触发点击事件的index 42 | /// - indexPath: 触发点击事件的indexPath 43 | func didSelected(banner: CWBanner, index: Int, indexPath: IndexPath) 44 | 45 | /// 开始滚动 46 | /// - Parameters: 47 | /// - banner: 控件对象 48 | /// - index: 开始时的index 49 | /// - indexPath: 开始时的indexPath 50 | func didStartScroll(banner: CWBanner, index: Int, indexPath: IndexPath) 51 | 52 | /// 结束滚动 53 | /// - Parameters: 54 | /// - banner: 控件对象 55 | /// - index: 结束后居中的index 56 | /// - indexPath: 居中后结束的indexPath 57 | func didEndScroll(banner: CWBanner, index: Int, indexPath: IndexPath) 58 | } 59 | 60 | protocol CWBannerPageControl where Self: UIView { 61 | /// 当前下标 62 | var currentPage: Int? {set get} 63 | /// 总数 64 | var numberOfPages: Int? {get set} 65 | /// 自定义pageControl被添加到控件上后会调此方法, 在这里可以设置pageControl的布局 66 | func setupLayoutWhenMoveToBannerView(_ banner: CWBanner) -> Void 67 | } 68 | 69 | class CWBanner: UIView { 70 | //MARK: - 构造方法 71 | init(frame: CGRect, flowLayout: CWSwiftFlowLayout, delegate: CWBannerDelegate) { 72 | self.flowLayout = flowLayout 73 | self.delegate = delegate 74 | super.init(frame: frame) 75 | self.configureBanner() 76 | } 77 | 78 | override func layoutSubviews() { 79 | super.layoutSubviews() 80 | self.resumePlay() 81 | } 82 | 83 | deinit { 84 | NSLog("[%@ -- %@]",NSStringFromClass(self.classForCoder), #function); 85 | self.releaseTimer() 86 | NotificationCenter.default.removeObserver(self) 87 | } 88 | 89 | required init?(coder aDecoder: NSCoder) { 90 | self.flowLayout = CWSwiftFlowLayout.init(style: .unknown) 91 | super.init(coder: aDecoder) 92 | } 93 | 94 | //MARK: - Property 95 | fileprivate lazy var proxy: CWProxy = { 96 | let p = CWProxy(self) 97 | return p 98 | }() 99 | /// 是否展示PageControl 默认:true 100 | var showPageControl = true 101 | /// 没有数据时的占位图 102 | let emptyImgView = UIImageView.init(frame: CGRect.zero) 103 | /// 自定义layout 104 | let flowLayout: CWSwiftFlowLayout 105 | /// collectionView 106 | lazy var banner: UICollectionView = { 107 | let b = CWCollectionView.init(frame: CGRect.zero, collectionViewLayout: self.flowLayout) 108 | b.translatesAutoresizingMaskIntoConstraints = false 109 | self.addSubview(b) 110 | self.sendSubviewToBack(b) 111 | b.delegate = self 112 | b.dataSource = self 113 | b.showsHorizontalScrollIndicator = false 114 | b.decelerationRate = UIScrollView.DecelerationRate(rawValue: 0) 115 | b.backgroundColor = self.backgroundColor 116 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[view]-0-|", 117 | options: [], 118 | metrics: nil, 119 | views: ["view" : b])) 120 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[view]-0-|", 121 | options: [], 122 | metrics: nil, 123 | views: ["view" : b])) 124 | b.register(UICollectionViewCell.classForCoder(), forCellWithReuseIdentifier: "tempCell") 125 | self.addSubview(self.emptyImgView) 126 | self.emptyImgView.translatesAutoresizingMaskIntoConstraints = false 127 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[empty]|", options: [], metrics: nil, views: ["empty":emptyImgView])) 128 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[empty]|", options: [], metrics: nil, views: ["empty":emptyImgView])) 129 | return b 130 | }() 131 | /// 外部代理委托 132 | weak var delegate: CWBannerDelegate? 133 | /// 当前居中展示的cell的下标 134 | var currentIndexPath: IndexPath = IndexPath.init(row: 0, section: 0) { 135 | didSet { 136 | let current = self.caculateIndex(indexPath: self.currentIndexPath) 137 | self.currentIndex = current 138 | if self.customPageControl == nil { 139 | self.pageControl.currentPage = current 140 | return 141 | } 142 | self.customPageControl?.currentPage = current 143 | } 144 | } 145 | fileprivate var currentIndex: Int = 0 146 | /// 是否开启自动滚动 (默认是关闭的) 147 | var autoPlay = false 148 | /// 定时器 149 | var timer: Timer? 150 | /// 自动滚动时间间隔,默认3s 151 | var timeInterval: TimeInterval = 3.0 152 | /// 默认的pageControl (默认位置在中下方,需要调整位置请自行设置frame) 153 | lazy var pageControl: UIPageControl = { 154 | let count = self.delegate?.bannerNumbers() 155 | let width = CGFloat(5) * CGFloat((count ?? 0)) 156 | let height: CGFloat = 10 157 | let pageControl = UIPageControl.init(frame: CGRect.init(x: 0, y: 0, width: width, height: height)) 158 | pageControl.center = CGPoint.init(x: self.bounds.width * 0.5, y: self.bounds.height - height * 0.5 - 8) 159 | pageControl.currentPage = 0 160 | pageControl.numberOfPages = self.delegate?.bannerNumbers() ?? 0 161 | pageControl.pageIndicatorTintColor = UIColor.white 162 | pageControl.isUserInteractionEnabled = false; 163 | pageControl.currentPageIndicatorTintColor = UIColor.black 164 | pageControl.translatesAutoresizingMaskIntoConstraints = false; 165 | return pageControl 166 | }() 167 | /// 自定义的pageControl 168 | var customPageControl: CWBannerPageControl? { 169 | willSet 170 | { 171 | guard let custom = newValue else {return} 172 | guard custom.superview == nil else {return} 173 | self.addSubview(custom); 174 | self.bringSubviewToFront(custom); 175 | self.pageControl.removeFromSuperview(); 176 | newValue?.setupLayoutWhenMoveToBannerView(self) 177 | newValue?.numberOfPages = self.numbers 178 | newValue?.currentPage = self.currentIndex 179 | } 180 | } 181 | 182 | /// 控件版本号 183 | var version: String { 184 | get{ 185 | return "1.1.9"; 186 | } 187 | } 188 | 189 | /// 是否无限轮播 true:无限衔接下去; false: 到最后一张后就没有了 190 | var endless: Bool = true 191 | } 192 | 193 | // MARK: - OPEN METHOD 194 | extension CWBanner { 195 | /// 刷新数据 196 | func freshBanner() { 197 | self.stop() 198 | self.banner.reloadData() 199 | self.banner.layoutIfNeeded() 200 | self.scrollToIndexPathNoAnimated(self.originIndexPath()) 201 | self.play() 202 | if self.customPageControl != nil { 203 | self.customPageControl?.numberOfPages = self.delegate?.bannerNumbers() ?? 0 204 | self.customPageControl?.currentPage = 0 205 | return 206 | } 207 | self.pageControl.numberOfPages = self.delegate?.bannerNumbers() ?? 0 208 | self.pageControl.currentPage = 0 209 | } 210 | 211 | fileprivate func play() { 212 | guard self.numbers > 1 && self.autoPlay else { 213 | self.releaseTimer() 214 | return 215 | } 216 | 217 | guard self.timer == nil else { 218 | return 219 | } 220 | 221 | defer { self.timer?.fireDate = Date.init(timeIntervalSinceNow: self.timeInterval) } 222 | 223 | if #available(iOS 10.0, *) { 224 | self.timer = Timer.scheduledTimer(withTimeInterval: self.timeInterval, repeats: true, block: { [weak self] (timer) in 225 | self?.nextCell() 226 | }) 227 | return 228 | } 229 | 230 | self.timer = self.proxy.createTimer(timeInterval: self.timeInterval, { t in 231 | t?.nextCell() 232 | }) 233 | } 234 | 235 | @objc fileprivate func nextCell() { 236 | 237 | defer { 238 | self.scrollViewWillBeginDecelerating(self.banner) 239 | } 240 | 241 | if self.endless { 242 | self.currentIndexPath = self.currentIndexPath + 1; 243 | return 244 | } 245 | 246 | let lastIndex = self.flowLayout.style == .normal ? self.numbers - 1 : self.factNumbers - 2 247 | 248 | if self.currentIndexPath.row == lastIndex { 249 | let row = self.flowLayout.style == .normal ? 0 : 1 250 | self.currentIndexPath = IndexPath.init(row: row, section: 0) 251 | return 252 | } 253 | self.currentIndexPath = self.currentIndexPath + 1 254 | } 255 | 256 | /// 继续滚动轮播图 257 | func resumePlay() { 258 | self.play() 259 | } 260 | 261 | /// 暂停自动滚动 262 | func pause() { 263 | if let timer = self.timer { 264 | timer.fireDate = Date.distantFuture 265 | } 266 | } 267 | 268 | /// 停止滚动 269 | func stop() { 270 | self.pause() 271 | } 272 | 273 | /// 释放timer资源,防止内存泄漏 274 | fileprivate func releaseTimer() { 275 | guard let timer = self.timer else { 276 | return 277 | } 278 | timer.fireDate = Date.distantFuture 279 | timer.invalidate() 280 | self.timer = nil 281 | } 282 | 283 | /// banner所处控制器WillAppear方法中调用 284 | func bannerWillAppear() { 285 | self.play() 286 | self.adjustErrorCell(isScroll: true, animation: false) 287 | } 288 | 289 | /// banner所处控制器WillDisAppear方法中调用 290 | func bannerWillDisAppear() { 291 | self.pause() 292 | self.adjustErrorCell(isScroll: true, animation: false) 293 | } 294 | 295 | func scroll(to index: Int, animated: Bool = true) { 296 | let numbers = self.numbers 297 | 298 | guard numbers >= 0 && index < numbers else { 299 | return 300 | } 301 | 302 | guard index != self.currentIndex else { 303 | return 304 | } 305 | 306 | self.currentIndexPath = self.currentIndexPath + (index - self.currentIndex) 307 | if (animated) { 308 | self.scrollToIndexPathAnimated(self.currentIndexPath) 309 | return 310 | } 311 | self.scrollToIndexPathNoAnimated(self.currentIndexPath) 312 | } 313 | } 314 | 315 | // MARK: - LOGIC HELPER 316 | extension CWBanner { 317 | /// 代码层下标换算成业务层下标 318 | /// 319 | /// - Parameter IndexPath: 代码层cell对应的下标 320 | /// - Returns: 业务层对应的下标 321 | fileprivate func caculateIndex(indexPath: IndexPath) -> Int { 322 | guard self.numbers > 0 else { 323 | return 0 324 | } 325 | var row = indexPath.row % self.numbers 326 | if self.endless == false, self.flowLayout.style != .normal { 327 | row = indexPath.row % self.factNumbers - 1 328 | } 329 | return row 330 | } 331 | 332 | /// 第一次加载时,会从中间开始展示 333 | /// 334 | /// - Returns: 返回对应的indexPath 335 | fileprivate func originIndexPath() -> IndexPath { 336 | 337 | let numbers = self.numbers 338 | 339 | guard numbers > 0 else { 340 | return IndexPath.init(index: 0) 341 | } 342 | 343 | guard endless else { 344 | let row = self.flowLayout.style == .normal ? 0 : 1 345 | self.currentIndexPath = IndexPath.init(row: row, section: 0) 346 | return self.currentIndexPath 347 | } 348 | 349 | let centerIndex = self.factNumbers / numbers 350 | self.currentIndexPath = centerIndex == 1 ? IndexPath.init(row: 0, section: 0) : IndexPath.init(row: centerIndex / 2 * numbers, section: 0) 351 | 352 | return self.currentIndexPath 353 | } 354 | 355 | /// 边缘检测, 如果将要滑到边缘,调整位置 356 | fileprivate func checkOutOfBounds() { 357 | let row = self.currentIndexPath.row 358 | if row == self.factNumbers - 2 359 | || row == 1 { 360 | let originIndexPath = self.originIndexPath() 361 | var index = self.caculateIndex(indexPath: self.currentIndexPath) 362 | index = row == 1 ? index + 1 : index - 2 363 | self.currentIndexPath = originIndexPath + index 364 | self.scrollToIndexPathNoAnimated(self.currentIndexPath) 365 | } 366 | } 367 | 368 | fileprivate func scrollToIndexPathAnimated(_ indexPath: IndexPath) { 369 | guard self.numbers > 0 else { 370 | return 371 | } 372 | self.banner.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) 373 | } 374 | 375 | fileprivate func scrollToIndexPathNoAnimated(_ indexPath: IndexPath) { 376 | guard self.numbers > 0 else { 377 | return 378 | } 379 | self.banner.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) 380 | self.delegate?.didEndScroll(banner: self, index: self.currentIndex, indexPath: self.currentIndexPath) 381 | } 382 | 383 | /// cell错位检测和调整 384 | func adjustErrorCell(isScroll: Bool, animation: Bool = true) { 385 | let indexPaths = self.banner.indexPathsForVisibleItems 386 | let centerX: CGFloat = self.banner.contentOffset.x + self.banner.frame.width * 0.5 387 | var minSpace = CGFloat(MAXFLOAT) 388 | var newIndexPath: IndexPath = self.currentIndexPath 389 | indexPaths.forEach({ (idx) in 390 | if let atr = self.banner.layoutAttributesForItem(at: idx) { 391 | atr.zIndex = 0 392 | let diff = abs(atr.center.x - centerX) 393 | if abs(minSpace) > diff { 394 | minSpace = diff 395 | newIndexPath = atr.indexPath 396 | } 397 | } 398 | }) 399 | /// 正常情况下 newIndexPath 和 currentIndexPath 不会相差超过 2, 如果超过4明显不正常,就不做处理 400 | if abs(newIndexPath.row - self.currentIndexPath.row) < 4 { 401 | self.currentIndexPath = newIndexPath 402 | } 403 | 404 | if isScroll { 405 | self.cw_scrollViewWillBeginDecelerating(self.banner, animation: animation) 406 | } 407 | } 408 | 409 | @objc fileprivate func appActive(_ notify: Notification) { 410 | self.adjustErrorCell(isScroll: true, animation: false) 411 | self.play() 412 | } 413 | 414 | @objc fileprivate func appInactive(_ notify: Notification) { 415 | self.pause() 416 | self.adjustErrorCell(isScroll: true, animation: false) 417 | } 418 | } 419 | 420 | // MARK: - UI 421 | extension CWBanner { 422 | fileprivate func configureBanner() { 423 | 424 | if self.customPageControl == nil { 425 | self.addSubview(self.pageControl) 426 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[control]-0-|", 427 | options: [], 428 | metrics: nil, 429 | views: ["control" : self.pageControl])) 430 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[control(30)]-0-|", 431 | options: [], 432 | metrics: nil, 433 | views: ["control" : self.pageControl])) 434 | }else { 435 | self.addSubview(self.customPageControl!) 436 | 437 | } 438 | NotificationCenter.default.addObserver(self, 439 | selector: #selector(appActive(_:)), name: UIApplication.didBecomeActiveNotification , 440 | object:nil) 441 | 442 | NotificationCenter.default.addObserver(self, 443 | selector: #selector(appInactive(_:)), 444 | name: UIApplication.willResignActiveNotification, 445 | object: nil) 446 | } 447 | } 448 | 449 | // MARK: - UIScrollViewDelegate 450 | extension CWBanner { 451 | 452 | /// 开始拖拽 453 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 454 | 455 | if #available(iOS 14.0, *) { 456 | self.banner.isPagingEnabled = false 457 | } else { 458 | self.banner.isPagingEnabled = true 459 | } 460 | 461 | self.pause() 462 | self.delegate?.didStartScroll(banner: self, 463 | index: self.currentIndex, 464 | indexPath: self.currentIndexPath) 465 | } 466 | 467 | /// 将要结束拖拽 468 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 469 | 470 | if (self.endless == false) { 471 | 472 | var maxIndex = self.numbers - 1 473 | var minIndex = 0 474 | 475 | if self.flowLayout.style != .normal { 476 | maxIndex = self.factNumbers - 2 477 | minIndex = 1 478 | } 479 | 480 | if velocity.x >= 0 && self.currentIndexPath.row == maxIndex { 481 | self.adjustErrorCell(isScroll: true) 482 | return 483 | } 484 | 485 | if velocity.x <= 0 && self.currentIndexPath.row == minIndex { 486 | self.adjustErrorCell(isScroll: true) 487 | return 488 | } 489 | } 490 | 491 | // 这里不用考虑越界问题,其他地方做了保护 492 | if velocity.x > 0 { 493 | //左滑,下一张 494 | self.currentIndexPath = self.currentIndexPath + 1 495 | }else if velocity.x < 0 { 496 | //右滑, 上一张 497 | self.currentIndexPath = self.currentIndexPath - 1 498 | }else if velocity.x == 0 { 499 | self.adjustErrorCell(isScroll: false) 500 | if #available(iOS 14.0, *) { 501 | self.scrollViewWillBeginDecelerating(self.banner); 502 | } 503 | } 504 | } 505 | 506 | /// 将要开始减速 507 | func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 508 | self.cw_scrollViewWillBeginDecelerating(scrollView, animation: true) 509 | } 510 | 511 | func cw_scrollViewWillBeginDecelerating(_ scrollView: UIScrollView, animation: Bool = true) { 512 | 513 | let factNumbers = self.factNumbers 514 | 515 | guard self.currentIndexPath.row >= 0, 516 | self.currentIndexPath.row < factNumbers else { 517 | // 越界保护 518 | return 519 | } 520 | 521 | if self.endless == false { 522 | if self.currentIndexPath.row == 0 && self.flowLayout.style != .normal { 523 | self.currentIndexPath = IndexPath.init(row: 1, section: 0) 524 | } else if self.currentIndexPath.row == factNumbers - 1 && self.flowLayout.style != .normal { 525 | self.currentIndexPath = IndexPath.init(row: factNumbers - 2, section: 0) 526 | } 527 | } 528 | 529 | // 在这里将需要显示的cell置为居中 530 | if animation { 531 | self.scrollToIndexPathAnimated(self.currentIndexPath) 532 | return 533 | } 534 | 535 | self.scrollToIndexPathNoAnimated(self.currentIndexPath) 536 | } 537 | 538 | /// 结束减速 539 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 540 | self.banner.isPagingEnabled = false 541 | } 542 | 543 | /// 滚动动画完成 544 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 545 | self.banner.isPagingEnabled = false 546 | if self.endless { 547 | self.checkOutOfBounds() 548 | } 549 | self.resumePlay() 550 | self.delegate?.didEndScroll(banner: self, 551 | index: self.caculateIndex(indexPath: self.currentIndexPath), 552 | indexPath: self.currentIndexPath) 553 | } 554 | 555 | /// 滚动中 556 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 557 | 558 | } 559 | 560 | } 561 | 562 | // MARK: - UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout 563 | extension CWBanner: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 564 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 565 | 566 | func dequeueCell(_ idxPath: IndexPath) -> UICollectionViewCell { 567 | return self.delegate?.bannerView(banner: self, 568 | index: self.caculateIndex(indexPath: idxPath), 569 | indexPath: idxPath) ?? UICollectionViewCell() 570 | } 571 | 572 | if self.endless == false, 573 | self.flowLayout.style != .normal, 574 | indexPath.row == 0 || indexPath.row == self.factNumbers - 1 { 575 | 576 | return collectionView.dequeueReusableCell(withReuseIdentifier: "tempCell", for: indexPath) 577 | } 578 | 579 | return dequeueCell(indexPath) 580 | } 581 | 582 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 583 | return self.factNumbers 584 | } 585 | 586 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 587 | 588 | if self.endless == false, 589 | self.flowLayout.style != .normal, 590 | indexPath.row == 0 || indexPath.row == self.factNumbers - 1 { 591 | // 非无限轮播情况下, 第一个和最后一个是占位cell, 不参与外部的业务逻辑 592 | return 593 | } 594 | 595 | self.delegate?.didSelected(banner: self, 596 | index: self.caculateIndex(indexPath: indexPath), 597 | indexPath: indexPath) 598 | // 处于动画中时,点击cell,可能会出现cell不居中问题.这里处理下 599 | // 将里中心点最近的那个cell居 600 | self.adjustErrorCell(isScroll: true) 601 | } 602 | } 603 | 604 | // MARK: - Category 605 | extension CWBanner { 606 | /// 背地里实际返回的cell个数 607 | fileprivate var factNumbers: Int { 608 | 609 | let numbers = self.numbers 610 | 611 | guard numbers > 0 else { 612 | return 0; 613 | } 614 | 615 | func setPagecontrol(isHidden: Bool) { 616 | self.pageControl.isHidden = isHidden 617 | self.customPageControl?.isHidden = isHidden 618 | } 619 | 620 | self.banner.isScrollEnabled = numbers > 1 621 | setPagecontrol(isHidden: !self.showPageControl || numbers == 1) 622 | 623 | if endless { 624 | return numbers == 1 ? numbers : 100 625 | } 626 | 627 | return self.flowLayout.style == .normal ? numbers : numbers + 2 628 | } 629 | 630 | /// 业务层实际需要展示的cell个数 631 | fileprivate var numbers: Int { 632 | let count = self.delegate?.bannerNumbers() ?? 0 633 | self.emptyImgView.isHidden = count <= 0 634 | return count > 0 ? count : 0 635 | } 636 | } 637 | 638 | extension IndexPath { 639 | /// 重载 + 号运算符 640 | static func + (left: IndexPath, right: Int) -> IndexPath { 641 | return IndexPath.init(row: left.row + right, section: left.section) 642 | } 643 | 644 | /// 重载 - 号运算符 645 | static func - (left: IndexPath, right: Int) -> IndexPath { 646 | return IndexPath.init(row: left.row - right, section: left.section) 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /OC_CWCarousel/CWCarousel.m: -------------------------------------------------------------------------------- 1 | // 2 | // CWCarousel.m 3 | // CWCarousel 4 | // 5 | // Created by WangChen on 2018/4/3. 6 | // Copyright © 2018年 ChenWang. All rights reserved. 7 | // 8 | 9 | #import "CWCarousel.h" 10 | 11 | @interface CWCarouselCollectionView() 12 | @property (nonatomic, copy) void (^ _Nullable tapCallback) (void); 13 | @end 14 | 15 | 16 | @implementation CWCarouselCollectionView 17 | - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { 18 | if(self = [super initWithFrame:frame collectionViewLayout:layout]) { 19 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)]; 20 | [self addGestureRecognizer:tap]; 21 | tap.delegate = self; 22 | } 23 | return self; 24 | } 25 | 26 | - (void)tapAction:(UITapGestureRecognizer *)tap { 27 | if (!self.tapCallback) { 28 | return; 29 | } 30 | self.tapCallback(); 31 | } 32 | 33 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 34 | 35 | if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] == NO) { 36 | return YES; 37 | } 38 | 39 | if ([[touch view] isKindOfClass:[UICollectionView class]]) { 40 | return YES; 41 | } 42 | 43 | return NO; 44 | } 45 | 46 | - (void)dealloc { 47 | NSLog(@"[CWCarouselCollectionView dealloc]"); 48 | } 49 | @end 50 | 51 | 52 | @interface CWTempleteCell: UICollectionViewCell 53 | @end 54 | 55 | @implementation CWTempleteCell 56 | @end 57 | 58 | 59 | @interface MyProxy : NSProxy 60 | @property (nonatomic, weak) id _Nullable target; 61 | - (instancetype)init:(id)target; 62 | @end 63 | 64 | @implementation MyProxy 65 | - (instancetype)init:(id)target { 66 | self.target = target; 67 | return self; 68 | } 69 | 70 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { 71 | return [self.target methodSignatureForSelector:sel]; 72 | } 73 | 74 | - (void)forwardInvocation:(NSInvocation *)invocation { 75 | [invocation invokeWithTarget:self.target]; 76 | } 77 | 78 | @end 79 | 80 | 81 | @interface CWCarousel () { 82 | 83 | } 84 | /// collectionView 85 | @property (nonatomic, strong) CWCarouselCollectionView *carouselView; 86 | /// 轮播总数 87 | @property (nonatomic, assign) NSInteger numbers; 88 | /// 当前居中的业务逻辑下标 89 | @property (nonatomic, assign) NSInteger currentIndex; 90 | /// 当前居中的实际下标 91 | @property (nonatomic, assign) NSInteger infactIndex; 92 | /// (已经废弃) 93 | @property (nonatomic, assign) CGFloat addHeight; 94 | /** 95 | 自动播放是否暂停 96 | */ 97 | @property (nonatomic, assign) BOOL isPause; 98 | 99 | /** 100 | 当前展示在中间的cell下标 101 | */ 102 | @property (nonatomic, strong) NSIndexPath *currentIndexPath; 103 | 104 | @property (nonatomic, strong) MyProxy *timerProxy; 105 | @property (nonatomic, strong) NSTimer *timer; 106 | 107 | @end 108 | @implementation CWCarousel 109 | @synthesize carouselView = _carouselView; 110 | 111 | 112 | - (instancetype)initWithFrame:(CGRect)frame delegate:(id)delegate datasource:(id)datasource flowLayout:(CWFlowLayout *)flowLayout { 113 | CGFloat addHeight = 0; 114 | frame.size.height += addHeight; 115 | self.addHeight = addHeight; 116 | if(self = [super initWithFrame:frame]) { 117 | _flowLayout = flowLayout; 118 | self.delegate = delegate; 119 | self.datasource = datasource; 120 | self.isAuto = NO; 121 | self.autoTimInterval = 3; 122 | self.endless = YES; 123 | [self configureView]; 124 | [self addNotify]; 125 | } 126 | NSAssert(self != nil, @"CWCarousel 初始化失败!"); 127 | return self; 128 | } 129 | 130 | - (void)appBecomeInactive:(NSNotification *)notification { 131 | [self adjustErrorCell:YES]; 132 | } 133 | 134 | - (void)appBecomeActive:(NSNotification *)notification { 135 | [self adjustErrorCell:YES]; 136 | } 137 | 138 | - (void)controllerWillAppear { 139 | if(self.isAuto) { 140 | [self resumePlay]; 141 | } 142 | [self addNotify]; 143 | [self adjustErrorCell:YES]; 144 | } 145 | 146 | - (void)controllerWillDisAppear { 147 | if(self.isAuto) { 148 | [self pause]; 149 | } 150 | [self removeNotify]; 151 | [self adjustErrorCell:YES]; 152 | } 153 | 154 | - (void)addNotify { 155 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeInactive:) name:UIApplicationWillResignActiveNotification object:nil]; 156 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; 157 | } 158 | 159 | - (void)removeNotify { 160 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; 161 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; 162 | } 163 | 164 | - (void)dealloc { 165 | NSLog(@"[CWCarousel dealloc]"); 166 | [self removeNotify]; 167 | [self releaseTimer]; 168 | } 169 | 170 | - (void)willMoveToSuperview:(UIView *)newSuperview { 171 | [super willMoveToSuperview:newSuperview]; 172 | if (nil == self.customPageControl && nil == self.pageControl.superview) { 173 | [self configurePageControl]; 174 | } 175 | } 176 | 177 | - (void)willMoveToWindow:(UIWindow *)newWindow { 178 | [super willMoveToWindow:newWindow]; 179 | if (nil == self.customPageControl && nil == self.pageControl.superview) { 180 | [self configurePageControl]; 181 | } 182 | } 183 | 184 | - (void)registerViewClass:(Class)viewClass identifier:(NSString *)identifier { 185 | [self.carouselView registerClass:viewClass forCellWithReuseIdentifier:identifier]; 186 | } 187 | 188 | - (void)registerNibView:(NSString *)nibName identifier:(NSString *)identifier { 189 | [self.carouselView registerNib:[UINib nibWithNibName:nibName bundle:[NSBundle mainBundle]] 190 | forCellWithReuseIdentifier:identifier]; 191 | } 192 | 193 | - (void)freshCarousel { 194 | 195 | if([self numbers] < 0) { 196 | return; 197 | } 198 | 199 | [self.carouselView reloadData]; 200 | [self layoutIfNeeded]; 201 | 202 | if (self.endless) 203 | [self.carouselView scrollToItemAtIndexPath:[self originIndexPath] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; 204 | else { 205 | if(self.flowLayout.style == CWCarouselStyle_Normal) { 206 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; 207 | } 208 | else { 209 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath = [NSIndexPath indexPathForRow:1 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; 210 | } 211 | } 212 | 213 | self.carouselView.userInteractionEnabled = YES; 214 | if (self.isAuto) { 215 | [self play]; 216 | } 217 | } 218 | #pragma mark - < Scroll Delegate > 219 | /// 开始拖拽 220 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { 221 | // 防止拖动加速度太大,一次跳过多张图片,这里设置一下 222 | if (@available(iOS 14.0, *)) { 223 | scrollView.pagingEnabled = NO; 224 | } else { 225 | scrollView.pagingEnabled = YES; 226 | } 227 | if (self.isAuto) { 228 | [self stop]; 229 | } 230 | if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didStartScrollAtIndex:indexPathRow:)]) { 231 | [self.delegate CWCarousel:self didStartScrollAtIndex:[self caculateIndex:self.currentIndexPath.row] indexPathRow:self.currentIndexPath.row]; 232 | } 233 | } 234 | 235 | /// 将要结束拖拽 236 | - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { 237 | 238 | NSInteger num = [self numbers]; 239 | 240 | if (num <= 0) { 241 | return; 242 | } 243 | 244 | if (self.endless == NO) 245 | { 246 | // 非无限轮播 247 | NSInteger maxIndex = num - 1; 248 | NSInteger minIndex = 0; 249 | if(self.flowLayout.style != CWCarouselStyle_Normal) 250 | { 251 | // 后面有一个占位cell, 所以减2, 不是减1 252 | maxIndex = [self infactNumbers] - 2; 253 | // 前面有一个占位cell, 所以下标是从1开始 254 | minIndex = 1; 255 | } 256 | if (velocity.x == 0) { 257 | [self velocityZero]; 258 | return; 259 | } 260 | 261 | if (velocity.x >= 0 && self.currentIndexPath.row == maxIndex) { 262 | // 已经是最后一张了 263 | return; 264 | } 265 | 266 | if (velocity.x <= 0 && self.currentIndexPath.row == minIndex) { 267 | // 已经是第一张了 268 | return; 269 | } 270 | } 271 | 272 | if(velocity.x > 0) { 273 | //左滑,下一张 274 | self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:self.currentIndexPath.section]; 275 | }else if (velocity.x < 0) { 276 | //右滑,上一张 277 | self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row - 1 inSection:self.currentIndexPath.section]; 278 | }else if (velocity.x == 0) { 279 | [self velocityZero]; 280 | } 281 | } 282 | 283 | /// 开始减速 284 | - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView { 285 | [self cusScrollViewWillBeginDecelerating:YES scroll:scrollView]; 286 | } 287 | 288 | - (void)cusScrollViewWillBeginDecelerating:(BOOL)animation scroll:(UIScrollView *)scrollView{ 289 | 290 | if (self.currentIndexPath == nil) { 291 | return; 292 | } 293 | 294 | if (self.currentIndexPath.row >= [self infactNumbers]) { 295 | return; 296 | } 297 | 298 | if (self.currentIndexPath.row < 0) { 299 | return; 300 | } 301 | 302 | // 中间一张轮播,居中显示 303 | if (self.endless == NO) 304 | { 305 | // 非无限轮播, 非CWCarouselStyle_Normal样式下, 前后有两张占位cell, 这里需要处理一下. 306 | if (self.currentIndexPath.row == 0 && self.style != CWCarouselStyle_Normal) { 307 | 308 | self.currentIndexPath = [NSIndexPath indexPathForRow:1 inSection:self.currentIndexPath.section]; 309 | 310 | }else if (self.currentIndexPath.row == [self infactNumbers] - 1 && self.style != CWCarouselStyle_Normal) { 311 | 312 | self.currentIndexPath = [NSIndexPath indexPathForRow:[self infactNumbers] - 2 inSection:self.currentIndexPath.section]; 313 | 314 | } 315 | } 316 | 317 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animation]; 318 | 319 | if (animation == NO) { 320 | [self cusScrollAnimationEnd:scrollView]; 321 | } 322 | } 323 | 324 | /// 减速完成 325 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { 326 | 327 | // 打开交互 328 | scrollView.pagingEnabled = NO; 329 | if(self.isAuto) { 330 | [self play]; 331 | } 332 | } 333 | 334 | /// 滚动动画完成 335 | - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { 336 | 337 | [self cusScrollAnimationEnd:scrollView]; 338 | } 339 | 340 | - (void)cusScrollAnimationEnd:(UIScrollView *)scrollView { 341 | // 滚动完成,打开交互,关掉pagingEnabled 342 | // 为什么要关掉pagingEnabled呢,因为切换控制器的时候会有系统级bug,不信你试试. 343 | scrollView.userInteractionEnabled = YES; 344 | scrollView.pagingEnabled = NO; 345 | 346 | if(self.isAuto) { 347 | [self play]; 348 | } 349 | 350 | if (self.endless) { 351 | [self checkOutofBounds]; 352 | } 353 | 354 | if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didEndScrollAtIndex:indexPathRow:)]) { 355 | [self.delegate CWCarousel:self didEndScrollAtIndex:[self caculateIndex:self.currentIndexPath.row] indexPathRow:self.currentIndexPath.row]; 356 | } 357 | } 358 | 359 | // 滚动中 360 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 361 | 362 | } 363 | 364 | #pragma mark - < Logic Helper > 365 | - (NSIndexPath *)originIndexPath { 366 | 367 | NSInteger num = [self numbers]; 368 | if (num <= 0) { 369 | return [[NSIndexPath alloc] initWithIndex:0]; 370 | } 371 | 372 | if (self.endless == NO) { 373 | NSInteger row = self.flowLayout.style == CWCarouselStyle_Normal ? 0 : 1; 374 | self.currentIndexPath = [NSIndexPath indexPathForRow:row inSection:0]; 375 | return self.currentIndexPath; 376 | } 377 | 378 | NSInteger centerIndex = [self infactNumbers] / num; //一共有多少组 379 | 380 | if (centerIndex == 0) { 381 | // 异常, 一组都没有 382 | NSAssert(true, @"计算起始下标异常, 分组不足一组."); 383 | return self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; 384 | } 385 | 386 | if (centerIndex == 1) { 387 | return self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; 388 | } 389 | 390 | // 取中间一组展示 391 | self.currentIndexPath = [NSIndexPath indexPathForRow:centerIndex / 2 * num inSection:0]; 392 | return self.currentIndexPath; 393 | } 394 | 395 | // 检测是否到了边界 396 | - (void)checkOutofBounds { 397 | if ([self numbers] <= 0) {return;} 398 | 399 | BOOL scroll = NO; 400 | NSInteger index = self.currentIndex; 401 | 402 | // 越界检查 403 | if(self.currentIndexPath.row == [self infactNumbers] - 1) { 404 | index = [self caculateIndex:self.currentIndexPath.row] - 1; //最后一张 405 | scroll = YES; 406 | }else if(self.currentIndexPath.row == 0) { 407 | index = [self caculateIndex:self.currentIndexPath.row]; //第一张 408 | scroll = YES; 409 | } 410 | 411 | self.carouselView.userInteractionEnabled = YES; 412 | if (scroll == NO) { 413 | return; 414 | } 415 | 416 | NSIndexPath *origin = [self originIndexPath]; 417 | self.currentIndexPath = [NSIndexPath indexPathForRow:origin.row + index inSection:origin.section]; 418 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; 419 | } 420 | 421 | /** 422 | 实际下标转换成业务需求下标 423 | 424 | @param factIndex 实际下标 425 | @return 业务需求下标 426 | */ 427 | - (NSInteger)caculateIndex:(NSInteger)factIndex { 428 | NSInteger num = [self numbers]; 429 | if (num <= 0) { 430 | return 0; 431 | } 432 | NSInteger row = factIndex % num; 433 | if(self.endless == NO && self.flowLayout.style != CWCarouselStyle_Normal) { 434 | // 这种情况有占位cell 435 | row = factIndex % [self infactNumbers] - 1; 436 | } 437 | return row; 438 | } 439 | 440 | - (void)adjustErrorCell:(BOOL)isScroll { 441 | NSArray *indexPaths = [self.carouselView indexPathsForVisibleItems]; 442 | CGFloat centerX = self.carouselView.contentOffset.x + CGRectGetWidth(self.carouselView.frame) * 0.5; 443 | __block CGFloat minSpace = MAXFLOAT; 444 | [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 445 | UICollectionViewLayoutAttributes *attri = [self.carouselView layoutAttributesForItemAtIndexPath:obj]; 446 | attri.zIndex = 0; 447 | if(ABS(minSpace) > ABS(attri.center.x - centerX)) { 448 | minSpace = attri.center.x - centerX; 449 | self.currentIndexPath = attri.indexPath; 450 | } 451 | }]; 452 | if(isScroll) { 453 | [self scrollViewWillBeginDecelerating:self.carouselView]; 454 | } 455 | } 456 | 457 | /// velocity == 0时的处理 458 | - (void)velocityZero { 459 | // 还有一种情况,当滑动后手指按住不放,然后松开,此时的加速度其实是为0的 460 | [self adjustErrorCell:NO]; 461 | if (@available(iOS 14.0, *)) { 462 | // iOS14以前,就算加速度为0,后续系统会还是会走scrollViewWillBeginDecelerating:回调 463 | // 但是iOS14以后,加速度为0时,不会在后续执行回调.这里手动触发一下 464 | [self scrollViewWillBeginDecelerating:self.carouselView]; 465 | } 466 | } 467 | 468 | - (void)play { 469 | if(self.isPause) { 470 | return; 471 | } 472 | 473 | if (NO == self.isAuto) { 474 | return; 475 | } 476 | 477 | if (self.timer) { 478 | [self resumePlay]; 479 | return; 480 | } 481 | self.timer = [NSTimer timerWithTimeInterval:self.autoTimInterval target:self.timerProxy selector:@selector(nextCell) userInfo:nil repeats:YES]; 482 | [self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:self.autoTimInterval]]; 483 | [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 484 | } 485 | 486 | - (void)nextCell { 487 | 488 | if([self numbers] <= 0) { 489 | return; 490 | } 491 | 492 | if (!self.isAuto) { 493 | return; 494 | } 495 | 496 | NSInteger maxIndex = 1; 497 | if(!self.endless && self.flowLayout.style != CWCarouselStyle_Normal) 498 | { 499 | maxIndex = 2; 500 | } 501 | if(self.currentIndexPath.row < [self infactNumbers] - maxIndex) 502 | { 503 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:self.currentIndexPath.section]; 504 | [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; 505 | self.currentIndexPath = indexPath; 506 | } 507 | else if(!self.endless) 508 | { 509 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:maxIndex - 1 inSection:self.currentIndexPath.section]; 510 | [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; 511 | self.currentIndexPath = indexPath; 512 | } 513 | } 514 | 515 | - (void)stop { 516 | if (!self.timer) { 517 | return; 518 | } 519 | [self.timer setFireDate:[NSDate distantFuture]]; 520 | } 521 | 522 | - (void)resumePlay { 523 | self.isPause = NO; 524 | if (self.timer) { 525 | [self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:self.autoTimInterval]]; 526 | return; 527 | } 528 | [self play]; 529 | } 530 | 531 | - (void)pause { 532 | self.isPause = YES; 533 | [self stop]; 534 | } 535 | 536 | - (void)releaseTimer { 537 | // [self stop]; 538 | if (!self.timer) { 539 | return; 540 | } 541 | [self.timer setFireDate:[NSDate distantFuture]]; 542 | [self.timer invalidate]; 543 | self.timer = nil; 544 | } 545 | 546 | - (void)scrollTo:(NSInteger)index animation:(BOOL)animation { 547 | 548 | if (index < 0 || index >= [self numbers]) { 549 | // 防止越界 550 | return; 551 | } 552 | 553 | [self stop]; 554 | 555 | if (index == self.currentIndex) { 556 | [self play]; 557 | return; 558 | } 559 | 560 | self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + (index - self.currentIndex) inSection:0]; 561 | [self cusScrollViewWillBeginDecelerating:animation scroll:self.carouselView]; 562 | } 563 | 564 | - (void)configurePageControl { 565 | UIView *control = self.customPageControl; 566 | BOOL isDefault = NO; 567 | 568 | if (nil == control || NO == [control isKindOfClass:[UIView class]]) { 569 | control = self.pageControl; 570 | isDefault = YES; 571 | } 572 | 573 | if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:addPageControl:isDefault:)]) { 574 | [self.delegate CWCarousel:self addPageControl:control isDefault:isDefault]; 575 | return; 576 | } 577 | 578 | [self addSubview:control]; 579 | control.translatesAutoresizingMaskIntoConstraints = NO; 580 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[control(20)]-0-|" 581 | options:kNilOptions 582 | metrics:nil 583 | views:@{@"control" : control}]]; 584 | [[control.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES]; 585 | } 586 | 587 | #pragma mark - < Configure View> 588 | - (void)configureView { 589 | self.backgroundColor = [UIColor blackColor]; 590 | self.carouselView.showsVerticalScrollIndicator = NO; 591 | self.carouselView.showsHorizontalScrollIndicator = NO; 592 | self.carouselView.decelerationRate = 0; 593 | } 594 | 595 | #pragma mark - < Delegate, Datasource > 596 | - (nonnull __kindof UICollectionViewCell *)collectionView:(nonnull UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath { 597 | 598 | __weak __typeof(&*self) weakSelf = self; 599 | 600 | UICollectionViewCell* (^returnCell)(NSIndexPath *) = ^UICollectionViewCell* (NSIndexPath *idx) { 601 | if (self.datasource && [self.datasource respondsToSelector:@selector(viewForCarousel:indexPath:index:)]) { 602 | UICollectionViewCell *cell = [weakSelf.datasource viewForCarousel:weakSelf indexPath:indexPath index:[weakSelf caculateIndex:indexPath.row]]; 603 | return cell; 604 | } 605 | return nil; 606 | }; 607 | 608 | 609 | if (self.endless) { 610 | return returnCell(indexPath); 611 | } 612 | 613 | if (self.flowLayout.style != CWCarouselStyle_Normal 614 | && (indexPath.row == 0 || indexPath.row == [self infactNumbers] - 1)) { 615 | // 非无限轮播情况下, "第一个"和"最后一个"是占位cell 616 | UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"tempCell" forIndexPath:indexPath]; 617 | cell.contentView.backgroundColor = [UIColor clearColor]; 618 | return cell; 619 | } 620 | 621 | return returnCell(indexPath); 622 | 623 | } 624 | 625 | - (NSInteger)collectionView:(nonnull UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { 626 | return [self infactNumbers]; 627 | } 628 | 629 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { 630 | if(self.delegate && 631 | [self.delegate respondsToSelector:@selector(CWCarousel:didSelectedAtIndex:)]) { 632 | [self.delegate CWCarousel:self didSelectedAtIndex:[self caculateIndex:indexPath.row]]; 633 | } 634 | // 处于动画中时,点击cell,可能会出现cell不居中问题.这里处理下 635 | // 将里中心点最近的那个cell居中 636 | [self adjustErrorCell:YES]; 637 | } 638 | 639 | #pragma mark - 640 | - (void)setBackgroundColor:(UIColor *)backgroundColor { 641 | self.carouselView.backgroundColor = backgroundColor; 642 | [super setBackgroundColor:backgroundColor]; 643 | } 644 | 645 | - (void)setCurrentIndexPath:(NSIndexPath *)currentIndexPath { 646 | _currentIndexPath = currentIndexPath; 647 | 648 | if (_currentIndexPath) { 649 | self.currentIndex = [self caculateIndex:_currentIndexPath.row]; 650 | } 651 | 652 | if(self.customPageControl == nil) 653 | self.pageControl.currentPage = [self caculateIndex:currentIndexPath.row]; 654 | else 655 | self.customPageControl.currentPage = [self caculateIndex:currentIndexPath.row]; 656 | } 657 | 658 | - (void)setEndless:(BOOL)endless { 659 | if(_endless != endless) { 660 | _endless = endless; 661 | } 662 | } 663 | 664 | - (void)setCustomPageControl:(UIView *)customPageControl { 665 | 666 | if (_customPageControl == customPageControl) { 667 | return; 668 | } 669 | 670 | if (_customPageControl) { 671 | [_customPageControl removeFromSuperview]; 672 | } 673 | 674 | _customPageControl = customPageControl; 675 | if (customPageControl && self.pageControl) { 676 | [self.pageControl removeFromSuperview]; 677 | self.pageControl = nil; 678 | } 679 | 680 | [self configurePageControl]; 681 | } 682 | 683 | #pragma mark - < getter > 684 | - (UICollectionView *)carouselView { 685 | if(!_carouselView) { 686 | self.carouselView = [[CWCarouselCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.flowLayout]; 687 | _carouselView.delegate = self; 688 | _carouselView.dataSource = self; 689 | _carouselView.translatesAutoresizingMaskIntoConstraints = NO; 690 | [_carouselView registerClass:[CWTempleteCell class] forCellWithReuseIdentifier:@"tempCell"]; 691 | [self addSubview:_carouselView]; 692 | 693 | NSDictionary *views = @{@"view" : self.carouselView}; 694 | NSDictionary *margins = @{@"top" : @(self.addHeight * 0.5), 695 | @"bottom" : @(self.addHeight * 0.5) 696 | }; 697 | NSString *str = @"H:|-0-[view]-0-|"; 698 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:str 699 | options:kNilOptions 700 | metrics:margins 701 | views:views]]; 702 | str = @"V:|-top-[view]-top-|"; 703 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:str 704 | options:kNilOptions 705 | metrics:margins 706 | views:views]]; 707 | 708 | __weak __typeof(&*self) weakSelf = self; 709 | [_carouselView setTapCallback:^{ 710 | [weakSelf adjustErrorCell:YES]; 711 | }]; 712 | } 713 | return _carouselView; 714 | } 715 | 716 | 717 | - (CWCarouselStyle)style { 718 | if(self.flowLayout) { 719 | return self.flowLayout.style; 720 | } 721 | return CWCarouselStyle_Unknow; 722 | } 723 | 724 | 725 | /** 726 | Description 727 | 728 | @return 业务需求需要展示轮播图个数 729 | */ 730 | - (NSInteger)numbers { 731 | if(self.datasource && 732 | [self.datasource respondsToSelector:@selector(numbersForCarousel)]) { 733 | NSInteger num = [self.datasource numbersForCarousel]; 734 | if (self.pageControl) { 735 | self.pageControl.numberOfPages = num; 736 | } 737 | if (self.customPageControl) { 738 | self.customPageControl.pageNumbers = num; 739 | } 740 | return num; 741 | } 742 | return 0; 743 | } 744 | 745 | /** 746 | 轮播图实际加载视图个数 747 | 748 | @return 轮播图实际加载视图个数 749 | */ 750 | - (NSInteger)infactNumbers { 751 | 752 | NSInteger num = [self numbers]; 753 | 754 | if ( num <= 0) { 755 | return 0; 756 | } 757 | 758 | [self.carouselView setScrollEnabled:YES]; 759 | 760 | if (self.endless) { 761 | // 无限轮播 762 | if (num == 1) { 763 | // 只有一张, 不让滚动 764 | [self.carouselView setScrollEnabled:NO]; 765 | return num; 766 | } 767 | // 不止一张, 默认加载300个 (cell有复用机制, 不用担心) 768 | return 300; 769 | } 770 | 771 | // 非无限轮播, 除了第一种样式, 其他的样式要加2个占位空cell 772 | if(self.flowLayout.style == CWCarouselStyle_Normal) { 773 | return num; 774 | } 775 | 776 | if (num == 1) { 777 | [self.carouselView setScrollEnabled:NO]; 778 | } 779 | // 前后2个占位cell,所以+2 780 | return num + 2; 781 | } 782 | 783 | - (UIPageControl *)pageControl { 784 | if(!_pageControl) { 785 | self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectZero]; 786 | _pageControl.pageIndicatorTintColor = [UIColor blackColor]; 787 | _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor]; 788 | _pageControl.userInteractionEnabled = NO; 789 | } 790 | return _pageControl; 791 | } 792 | 793 | - (MyProxy *)timerProxy { 794 | if (!_timerProxy) { 795 | self.timerProxy = [[MyProxy alloc] init:self]; 796 | } 797 | return _timerProxy; 798 | } 799 | 800 | - (NSString *)version { 801 | return @"1.1.9"; 802 | } 803 | @end 804 | 805 | 806 | 807 | 808 | -------------------------------------------------------------------------------- /CWCarouselDemo/CWCarousel/Sources/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | --------------------------------------------------------------------------------