无限轮播图学习
在移动应用中,首页广告位等常常需要自动循环播放的轮播图,本文将分享iOS 中使用 UIScrollView 实现无限轮播图的思路和关键技巧
一、无限轮播图是什么
无限轮播图指的是图片可以自动滚动、循环播放且无明显断点的 UI 组件。用户可以通过手指滑动或定时器自动滚动来切换图片,而不会感知到首尾的存在。
在 iOS 中,轮播图的核心是 UIScrollView,将多张图片横向排列,每次滚动一个屏幕宽度。
二、核心实现原理
1. 复制首尾
为了实现无限循环,需要在真实图片前后各添加一张复制图片,例如:
真实数据: [A, B, C, D]
实际布局: [D复制, A, B, C, D, A复制]
通过这种方式,ScrollView 到达边界时,可以瞬间跳回对应的真实页,用户不会察觉
2.边界检测逻辑
利用scrollViewDidScroll: 方法检测偏移量:由于每次ScrollView滚动都会调用这个方法,在里面判断偏移量是否越界,越界了就从复制页跳到原页面
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = scrollView.bounds.size.width;
NSInteger total = self.realCount + 2; // 含首尾复制
// 滑到最左侧复制页 → 跳回真实最后一张
if (scrollView.contentOffset.x < pageWidth) {
[scrollView setContentOffset:CGPointMake(pageWidth * (total - 2), 0) animated:NO];
}
// 滑到最右侧复制页 → 跳回真实第一张
if (scrollView.contentOffset.x >= pageWidth * (total - 1)) {
[scrollView setContentOffset:CGPointMake(pageWidth, 0) animated:NO];
}
}
三、视图布局
- 在 ScrollView 内放一个
contentView,作为所有图片的容器 - 锁定
contentView高度等于 ScrollView,确保只能横向滚动 - 图片从左到右排列,首尾贴合
contentView左右两端
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.scrollView);
make.height.equalTo(self.scrollView);
}];
firstImageView.left.equalTo(self.contentView);
for (NSInteger i = 1; i < images.count; i++) {
imageView.left.equalTo(previousImageView.mas_right);
}
lastImageView.right.equalTo(self.contentView);
四、自动滚动与 Timer
使用 NSTimer 定时翻页:
self.autoScrollTimer = [NSTimer scheduledTimerWithTimeInterval:2.5
target:self
selector:@selector(scrollToNext)
userInfo:nil
repeats:YES];
翻页逻辑:
- (void)scrollToNext {
CGFloat pageWidth = self.scrollView.bounds.size.width;
[self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x + pageWidth, 0) animated:YES];
}
停止 Timer:
[self.autoScrollTimer invalidate];
self.autoScrollTimer = nil;
五、用户交互与生命周期处理
- 拖拽时暂停:防止手势与 Timer 冲突。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[self stopAutoScroll];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
[self startAutoScroll];
}
- 页面消失时释放:防止 Timer 强引用造成内存泄漏
六、加入UIPageControll
UIPageControl 是 iOS 用来表示“分页位置”的控件,常见的像这样
● ○ ○ ○
PageControl 不会自动跟 UIScrollView 联动,必须在 scrollView 代理里更新它
在这个轮播图中,PageControl 不能用真实 index:由于有复制页
因此必须映射
self.pageControl = [[UIPageControl alloc] init];
self.pageControl.numberOfPages = 4;
self.pageControl.currentPage = 0;
[self.view addSubview:self.pageControl];
[self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_bottom).offset(10);
make.centerX.equalTo(self.view);
}];
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = scrollView.frame.size.width;
CGFloat offsetX = scrollView.contentOffset.x;
NSInteger total = 4 + 2;
if (offsetX <= 0) {
CGFloat realLastX = pageWidth * (total - 2);
scrollView.contentOffset = CGPointMake(realLastX, 0);
}
if (offsetX >= pageWidth * (total - 1)) {
scrollView.contentOffset = CGPointMake(pageWidth, 0);
}
NSInteger index = scrollView.contentOffset.x / pageWidth;
NSInteger page;
if (index == 0) {
page = 3;
} else if (index == total - 1) {
page = 0;
} else {
page = index - 1;
}
self.pageControl.currentPage = page;
}
七、易错点总结
- 初始显示复制页 → 需要
dispatch_async延迟设置偏移量 - 滑到头尾卡住 → 忘记设置
delegate或声明<UIScrollViewDelegate> - contentView 宽度未撑开 → 最后一张图片忘记加
right约束 - 页面退出 Timer 未停止 → 内存泄漏
- 忘记打开
pagingEnabled = YES→ 滑动不对齐整页 - PageControl 用的真实 index→需要映射

在最后附上完整代码供运行
//
// ViewController.m
// 无限轮播图
//
// Created by MacBook Air on 2026/5/6.
//
#import "ViewController.h"
#import <Masonry/Masonry.h>
@interface ViewController () <UIScrollViewDelegate>
@property(nonatomic, strong) UIScrollView* scrollView;
@property(nonatomic, strong) UIView* contentView;
@property(nonatomic, strong) NSTimer* autoScrollTimer;
@property(nonatomic, strong) UIPageControl* pageControl;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始化ScrollView
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.pagingEnabled = YES;
self.scrollView.delegate = self;
self.scrollView.showsVerticalScrollIndicator = NO;
[self.view addSubview:self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(200);
make.left.right.equalTo(self.view);
make.height.mas_equalTo(400);
}];
self.contentView = [[UIView alloc] init];
[self.scrollView addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.scrollView);
make.height.equalTo(self.scrollView);
}];
UIView* previousView = nil;
for(int i = 0; i < 6; i++){
NSString* name;
if (i == 0) {
name = @"UI4";
} else if (i == 5) {
name = @"UI1";
} else {
name = [NSString stringWithFormat:@"UI%d", i];
}
UIImage* image = [UIImage imageNamed:name];
UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
[self.contentView addSubview:imageView];
[imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(self.contentView);
make.width.equalTo(self.scrollView);
if (previousView) {
make.left.equalTo(previousView.mas_right);
} else {
make.left.equalTo(self.contentView);
}
}];
previousView = imageView;
}
// 最后一张贴 contentView 右边收口
[previousView mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView.mas_right);
}];
//加入pageControl
self.pageControl = [[UIPageControl alloc] init];
self.pageControl.currentPageIndicatorTintColor = [UIColor redColor];
self.pageControl.pageIndicatorTintColor = [UIColor lightGrayColor];
self.pageControl.numberOfPages = 4;
self.pageControl.currentPage = 0;
[self.view addSubview:self.pageControl];
[self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_bottom).offset(10);
make.centerX.equalTo(self.view);
}];
}
- (void)scrollToRealFirst {
dispatch_async(dispatch_get_main_queue(), ^{
CGFloat width = self.scrollView.frame.size.width;
[self.scrollView setContentOffset:CGPointMake(width, 0) animated:NO];
});
}
- (void)startAutoScroll {
self.autoScrollTimer = [NSTimer scheduledTimerWithTimeInterval:2.5
target:self
selector:@selector(scrollToNext)
userInfo:nil
repeats:YES];
}
- (void)stopAutoScroll {
[self.autoScrollTimer invalidate];
self.autoScrollTimer = nil;
}
- (void)scrollToNext {
CGFloat width = self.scrollView.frame.size.width;
CGFloat nextX = self.scrollView.contentOffset.x + width;
[self.scrollView setContentOffset:CGPointMake(nextX, 0) animated:YES];
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = scrollView.frame.size.width;
CGFloat offsetX = scrollView.contentOffset.x;
NSInteger total = 4 + 2; //首尾各一张复制
// 滑到最左侧的复制页 → 跳回真实最后一张
if (offsetX < pageWidth) {
CGFloat realLastX = pageWidth * (total - 2);
[scrollView setContentOffset:CGPointMake(realLastX, 0) animated:NO];
}
// 滑到最右侧的复制页 → 跳回真实第一张
if (offsetX >= pageWidth * (total - 1)) {
[scrollView setContentOffset:CGPointMake(pageWidth, 0) animated:NO];
}
NSInteger index = scrollView.contentOffset.x / pageWidth;
NSInteger page;
if (index == 0) {
page = 3;
} else if (index == total - 1) {
page = 0;
} else {
page = index - 1;
}
self.pageControl.currentPage = page;
}
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[self stopAutoScroll];
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
[self stopAutoScroll];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self scrollToRealFirst];
[self startAutoScroll];
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self stopAutoScroll];
}
@end

525

被折叠的 条评论
为什么被折叠?



