无限轮播图学习

无限轮播图学习

在移动应用中,首页广告位等常常需要自动循环播放的轮播图,本文将分享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];
    }
}

三、视图布局

  1. 在 ScrollView 内放一个 contentView,作为所有图片的容器
  2. 锁定 contentView 高度等于 ScrollView,确保只能横向滚动
  3. 图片从左到右排列,首尾贴合 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;
}

七、易错点总结

  1. 初始显示复制页 → 需要 dispatch_async 延迟设置偏移量
  2. 滑到头尾卡住 → 忘记设置 delegate 或声明 <UIScrollViewDelegate>
  3. contentView 宽度未撑开 → 最后一张图片忘记加 right 约束
  4. 页面退出 Timer 未停止 → 内存泄漏
  5. 忘记打开 pagingEnabled = YES → 滑动不对齐整页
  6. 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值