UIScrollView 基本使用
苹果的官方文档:Scroll View Programming Guide
UIScrollView 是 iOS 非常重要的具备滚动能力的视图,能滚动的控件基本继承自 UIScrollView,比如UITableView, UICollectionView, UITextView等。
主题
响应滚动交互
var delegate: UIScrollViewDelegate?
内容大小和偏移量
var contentSize: CGSize
视图可以滚动的大小,决定了可以滚动的范围。
var contentOffset: CGPoint
滚动偏移量,
func setContentOffset(CGPoint, animated: Bool)
设置偏移量
管理 Content Inset Behavior
以下属性都用于控制边距的。
var adjustedContentInset: UIEdgeInsets
只读属性,iOS11+,表示
contentView.frame.origin
偏移了
scrollview.frame.origin
多少,也就是实际边距。根据
contentInsetAdjustmentBehavior
来决定是否需要添加 safeAreaInsets,
var contentInset: UIEdgeInsets
UIScrollView 四周边距
var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior
iOS 11+, 在设置边距的时候考虑 safeAreaInsets的方式,一共有以下四个值, 默认值为 automatic
enum UIScrollView.ContentInsetAdjustmentBehavior
状态值 | adjustedContentInset |
---|---|
automatic | 取决于 Controller,如果 Contoller 的 属性为true(默认值)且含有导航栏,则上下会自动添加 safeAreaInsets,即 adjustedContentInset =safeAreaInset + contentInset,不管是否滚动。其他情况与scrollableAxes一致 |
scrollableAxes | 滚动方向需要加上 SafeAreaInset |
never | adjustedContentInset = contentInset |
always | adjustedContentInset = safeAreaInset + contentInset |
func adjustedContentInsetDidChange()
adjustedContentInset 改变通知
配置 ScrollView
var isScrollEnabled: Bool
是否允许滑动
var isDirectionalLockEnabled: Bool
默认值为false, 如果设置为 true 只能同时滑动一边(如果垂直和水平方向都可以滑动时)
var isPagingEnabled: Bool
是否按照分页滑动
var scrollsToTop: Bool
是否开启 scroll-to-top 手势, 默认值 true, 这是 ScrollView 的一个影藏功能,点击状态栏,会自动回到顶部。
var bounces: Bool
是否开启弹框功能,默认值为 true,当需要滚动时才触发。
var alwaysBounceVertical: Bool
一直触发垂直弹簧效果。
var alwaysBounceHorizontal: Bool
一直触发水平弹簧效果。
滚动到固定位置
func scrollRectToVisible(CGRect, animated: Bool)
滚动到固定位置。
其他属性和方法参考官方文档
使用 UIScrollView + UIStackView 快速适配屏幕
众所周知,不同手机页面高度是不一致的,这就可能造成部分内容较多页面小屏幕可能展示不完。这时候就需要页面具有滚动功能。
借助 UIScrollView + UIStackView ,我们页面可分割为多个抽屉View,且不用关心页面布局和小屏幕手机显示不完的问题。
基础原理
层次结构包含如下图:

绿色:ViewController.view
蓝色:UIStackView,大小和 UIScrollView 重合,通过里面添加的子 View 决定是否滚动和,垂直方向的约束。
黑色:各个抽屉 View。
通过 view 决定 UIStackeView 大小, 设置 StackeView的宽度和 UIScrollView 一致,且四周约束等于 UIScrollView。这样就能决定 UIScrollView 的 contentsize,实现小屏幕自动滚动。
示例代码
Controller 完整代码
class BaseController: UIViewController { // 将该功能添加在基础 Controller中
lazy var contentView = lazyContentView() // 设置为懒加载,外部使用才加载。
let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
}
private func lazyContentView() -> UIStackView {
let contentView = UIStackView()
contentView.spacing = 8 // 设置默认间距
contentView.axis = .vertical // 垂直布局
scrollView.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
// 设置 contentView 四周与 scrollView 四周一致。
contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
// 宽度和高度约束中其中一个与 scrollView 相等
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true // 宽度相等,垂直滑动
// contentView.heightAnchor.constraint(equalToConstant: 800).isActive = true // 高度相等,水平滑动
view.addSubview(scrollView)
// 添加 ScrollView 的约束
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
// scrollView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true
return contentView
}
}
子类使用,添加 View
func setupView() {
let redView = UIView()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
redView.heightAnchor.constraint(equalToConstant: 300).isActive = true // 设置高度约束 或者内部垂直方向约束是确定的(能自己计算出高度),
contentView.addArrangedSubview(redView)
// contentView 添加其他具备垂直方向约束确定的 子View
}
UIStackView 扩展方法
UIStackView 通过
spacing
属性可以设置默认间距,如果需要设置不同间距,可以使用系统
customSpacing(after:)
方法,但是该方法 iOS11+,我们可以自定义一个方法兼容设置不同间距
设置额外间距方法
/// 在基础 spacing 之后再增加多少距离。
/// - Parameters:
/// - spacing: 额外增加的距离
/// - arrangedSubview: 目标视图
func setAppendSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) {
if #available(iOS 11.0, *) {
let customSpacing = self.spacing + spacing
setCustomSpacing(customSpacing, after: arrangedSubview)
} else {
guard let aimIndex = arrangedSubviews.firstIndex(of: arrangedSubview) else {
Log.assert("视图未添加,无法获取 index。应该先添加视图")
return
}
// swiftlint:disable init_by_MSUI
let emptyView = UIView()
emptyView.addSubview(arrangedSubview)
arrangedSubview.leftAnchor.constraint(equalTo: emptyView.leftAnchor).isActive = true
arrangedSubview.rightAnchor.constraint(equalTo: emptyView.rightAnchor).isActive = true
arrangedSubview.topAnchor.constraint(equalTo: emptyView.topAnchor).isActive = true
arrangedSubview.bottomAnchor.constraint(equalTo: emptyView.bottomAnchor, constant: -spacing).isActive = true
removeArrangedSubview(arrangedSubview)
insertArrangedSubview(emptyView, at: aimIndex)
}
}
添加一组子view 到 UIStackView
extension UIStackView {
func addArrangedSubviews(_ views: [UIView]) {
guard !views.isEmpty else {
return
}
views.forEach { addArrangedSubview($0) }
}
}
Tip: 向 UIStackView 中多次添加同一个子 view ,只有添加一个。
UIScrollView 实现分页功能
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scroll;
@property (weak, nonatomic) IBOutlet UIPageControl *page;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.automaticallyAdjustsScrollViewInsets=NO;
self.scroll.backgroundColor = [UIColor greenColor];
self.scroll.contentInset = UIEdgeInsetsMake(0, 0, 5, 10);
self.scroll.showsVerticalScrollIndicator = NO;//设置旁边显示条
self.scroll.showsHorizontalScrollIndicator = NO;//设置水平显示条
self.scroll.scrollEnabled=YES;
_scroll.pagingEnabled = YES;//支持分页,如果不设置,滑动是随意的。
self.scroll.bounces = NO;//弹簧效果(分页建议取消,可以避免由于滑动过度导致出现底层视图)
_scroll.delegate = self;
//准备好2个页
UIView *view1 = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 375, 298)];//注意这里的位置是相对与父视图ScrollView的位置,不是相对于屏幕。。。刚开始理解错啦
view1.backgroundColor = [UIColor orangeColor];
UIView *view2 = [[UIView alloc]initWithFrame:CGRectMake(375, 0, 375, 298)];
view2.backgroundColor = [UIColor yellowColor];
[_scroll addSubview:view1];
[_scroll addSubview:view2];
_scroll.contentSize = CGSizeMake(self.scroll.frame.size.width*2, 298);//等下试试大小(主要是宽度)可不可以改变
self.page.currentPage = 0;
self.page.numberOfPages = 2;//总用两页
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void) prepareTwoView{
}
#pragma ScrollViewDelegate
//实现代理
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(@"完成滚动,观察是否改变");
//[learn]怎么计算当前页值 偏移的x值除以宽度,
int index = fabs(self.scroll.contentOffset.x)/scrollView.frame.size.width;
self.page.currentPage = index;
}
@end
效国图如下:
3、可以使用UIScrollView的代理实现捏合缩放
若要使用,当前视图应遵守UIScrollViewDelegate代理协议,并设置代理属性(self.scrollView.delegate=self)
4、【扩展】缩放图片位置如何显示在中间、缩放图片横竖屏效果最佳、实现双击缩放、UIScrollView的嵌套使用(相同方向,相反方向,交叉方向)
地址链接
注意事项
1. Xib 使用 UIScrollView 在低版本奔溃
描述: 在Storyboard中使用到ScrollView,项目要求兼容最低版本为iOS 9, 在运行到使用ScrolleView的界面时,百分之百奔溃,奔溃日志"Could not instantiate class named _UIScrollViewLayoutGuide", 但是Storyboad并没有报错。
原因: storyboard自动使用ContentLayoutGuide, 只需要取消掉就好了。
修正后