O~1Swift 专栏iOS 开发每天分享优质文章

Swift中的UIScrollView的所有属性和方法详解

2017-07-13  本文已影响460人  langkee

前言

在上一篇文章中,我们学习了三方刷新库MJRefresh(巧用MJRefresh),同时我们也说了MJRefresh是基于UIScrollView的,在这篇文章中,我们将着重讲述一下UIScrollView的属性和方法的使用。

创建

UIScrollView继承自UIViewNSCoding协议, 因此它的创建相当简单

let testScrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
// 设置背景颜色
testScrollView.backgroundColor = UIColor.black
// 设置代理
testScrollView.delegate = self
view.addSubview(testScrollView)

属性

例如:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    
    // 向下拉动偏移量大于等于20
    if scrollView.contentOffset.y  >= -20 {
        print("now is \(scrollView.contentOffset.y)")
    }
}

例如:

testScrollView.contentSize = CGSize(width: 200 , height: 200 * 2)

例如:

testScrollView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

例如:

testScrollView.isDirectionalLockEnabled = true

这个属性需要解释一下,这样说吧,当你的手准备滑动的时候,手按住UIScrollView不放,如果一开始滑动的方向是x方向,那么你就无法在y方向上移动(此时手还没有放开);如果一开始滑动的方向是y方向,那么你就无法在x方向上滑动(此时手还没有放开);如果一开始滑动的方向是倾斜方向(x、y均同时移动),那么你可以在任何方向随意滑动(此时手还没有放开)!

例如:

testScrollView.bounces = false

例如:

在垂直方向有弹性效果

let testScrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testScrollView.backgroundColor = UIColor.black
testScrollView.delegate = self
testScrollView.bounces = true
testScrollView.alwaysBounceVertical = true
view.addSubview(testScrollView)
        
let testImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testImageView.backgroundColor = UIColor.green
testScrollView.addSubview(testImageView)

在垂直方向没有弹性效果

let testScrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testScrollView.backgroundColor = UIColor.black
testScrollView.delegate = self

// 因为contentSize为0,而且没有设置testScrollView.alwaysBounceVertical = true,所以即便testScrollView.bounces = true也没有弹性效果
testScrollView.bounces = true   

view.addSubview(testScrollView)
        
let testImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testImageView.backgroundColor = UIColor.green
testScrollView.addSubview(testImageView)

例如:

let testScrollView = UIScrollView(frame: CGRect(x: 50, y: 64, width: UIScreen.main.bounds.width - 100, height: 200))
testScrollView.backgroundColor = UIColor.black
testScrollView.contentSize = CGSize(width: UIScreen.main.bounds.width - 100 , height: 200 * 2)
testScrollView.delegate = self
testScrollView.isPagingEnabled = true

view.addSubview(testScrollView)

let testImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 100, height: 200))
testImageView.backgroundColor = UIColor.green
testScrollView.addSubview(testImageView)

let testView = UIImageView(frame: CGRect(x: 0, y: 200, width: UIScreen.main.bounds.width - 100, height: 200))
testView.backgroundColor = UIColor.red
testScrollView.addSubview(testView)

演示效果

例如:

// 当向下滑动时,滑动条距离顶部的距离总是20
testScrollView.scrollIndicatorInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)

indicatorStyle是个枚举类型:

public enum UIScrollViewIndicatorStyle : Int {

    case `default`  // 灰白色,搭配任意背景色

    case black      // 黑色,搭配白色背景最佳

    case white      // 白色,搭配黑色背景最佳
}
    1   UIScrollViewDecelerationRateNormal    值是 0.998
    2 UIScrollViewDecelerationRateFast      值是 0.99

例如设置

// 快速减速
testScrollView.decelerationRate = UIScrollViewDecelerationRateFast
public enum UIScrollViewIndexDisplayMode : Int {

    case automatic    // 根据需要自动显示或隐藏

    case alwaysHidden // 总是隐藏
}

事实上,这个属性我并没有试出任何的作用,我也不知道作用是什么?如果你知道,请告诉我!此外,我现在使用的环境是Xcode 8.3, 而这个属性似乎是在8.3以后才能用?看下面官方截图

结果:控制台没有输出任何日志,也就是没有响应button事件,并且可以看到button向上滑动,实质上是触发了UIScrollView的滑动事件!

02 不子类化UIScrollView,默认设置(true):操作方法,长按button向上滑动

结果:控制台输出“触发滑动视图上面的UIButton事件”, 响应了button事件,可以看到button没有向上滑动,也就是没有触发UIScrollView的滑动事件!

03 不子类化UIScrollView,设置false: 操作方法,点击button并快速向上滑动或者长按上滑

结果:当点击button并且同时快速上滑时或者长按上滑时,响应了button的事件,控制台输出“触发滑动视图上面的UIButton事件”, 没有看到button向上滑动,说明没有触发UIScrollView的滑动事件!

不子类化UIScrollView属性小结(个人认为): 一般情况下,我们使用该属性的默认值即可,因为如果不延迟内容触摸,优先响应UIScrollView上面的子控件,这样很容易造成误操作,比如上面的例子,一般用户的行为操作是希望向上滑动,但是只是不小心一开始触摸到的是一个button,却执行了button事件跳转到另一个页面或者toast提示或者其他操作,这显然不是用户所期望的,因为用户希望能够向上滑动,查看更多的状态等等!!!

不子类化UIScrollView注意点: 在上面的0102操作中,为什么在都是设置为true的情况下,点击button快速上滑是响应UIScrollView事件,但是长按button上滑却是响应button事件呢?这就牵扯到UIScrollView的内部原理了,在UIScrollView中,在有一个计时器用来判断用户当前操作在UIScrollView上面的子视图的响应事件时长(这里以button为例,设UIScrollView内部默认时长为x),当用户操作时长小于x时(比如点击按钮立即上滑),UIScrollView内部机制会将触发事件传递给UIScrollView处理,则响应UIScrollView事件; 当用户操作时长大于x(比如长按按钮上滑),UIScrollView内部机制就会将触发事件返回交由它的子视图button来处理,也即是认为用户是想要操作button而并非UIScrollView!

04 子类化UIScrollView,默认设置为true:操作方法,点击按钮立即上滑

代码:

class ViewController: UIViewController, UIScrollViewDelegate {
    
    private var testScrollView: SubScrollView!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 使用的是子类化UIScrollView
        testScrollView = SubScrollView(frame: CGRect(x: 50, y: 64, width: UIScreen.main.bounds.width - 100, height: 200))
        testScrollView.contentSize = CGSize(width: UIScreen.main.bounds.width - 100 , height: 200 * 4)
        testScrollView.delegate = self
        view.addSubview(testScrollView)
        
        let label1 = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 100, height: 200))
        label1.backgroundColor = UIColor.green
        label1.text = "第一页"
        label1.font = UIFont.boldSystemFont(ofSize: 17)
        testScrollView.addSubview(label1)

        let label2 = UILabel(frame: CGRect(x: 0, y: 200, width: UIScreen.main.bounds.width - 100, height: 200))
        label2.text = "第二页"
        label2.numberOfLines = 0
        label2.backgroundColor = UIColor.red
        label2.font = UIFont.boldSystemFont(ofSize: 17)
        testScrollView.addSubview(label2)
        
        let label3 = UILabel(frame: CGRect(x: 0, y: 400, width: UIScreen.main.bounds.width - 100, height: 200))
        label3.text = "第三页(最后一页)"
        label3.backgroundColor = UIColor.blue
        label3.font = UIFont.boldSystemFont(ofSize: 17)
        testScrollView.addSubview(label3)
        
        let btn = UIButton(type: .custom)
        btn.frame = CGRect(x: 100, y: 200 * 3 + 100, width: 100, height: 50)
        btn.backgroundColor = UIColor.red
        btn.setTitle("test button", for: .normal)
        btn.setTitleColor(UIColor.black, for: .normal)
        btn.addTarget(self, action: #selector(buttonAc), for: .touchUpInside)
        testScrollView.addSubview(btn)
    }
    
    func buttonAc() {
        print("触发滑动视图上面的UIButton事件")
    }
}

子类化UIScrollView代码:

class SubScrollView: UIScrollView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.blue
        // 默认为true
//        delaysContentTouches = false
    }
    
    override func touchesShouldCancel(in view: UIView) -> Bool {
        super.touchesShouldCancel(in: view)
        print("Test touches should cancel! current responding view is \(view)")
        return true
    }

    override func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?, in view: UIView) -> Bool {
        super.touchesShouldBegin(touches, with: event, in: view)
        print("Test touches should begin! current responding view is \(view)")
        return true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

好,看一下演示效果:

结果:点击按钮立即上滑,在默认设置(true)下,控制台没有输出button事件的打印值,按钮上移,所以是优先触发UIScrollView响应事件!另外,没有触发touchesShouldBegin方法,原因是delaysContentTouches没有设置成false,所以不会触发!

05 子类化UIScrollView,默认设置为true:操作方法,长按button上滑

演示效果:

结果:长按button上滑,没有触发button方法中的事件,button上移,控制台打印touchesShouldBegintouchesShouldCancel日志,说明响应的还是UIScrollView事件!这里又是怎么回事呢?还是之前提到的这是由于UIScrollView的内部机制,在04中,因为触发button的时间极其短,小于延迟内容触摸的时间x,所以UIScroll直接接收了事件的响应,没有将事件返回给它的子视图button!!而长按button的时候,触发button的时间已经达到内容触摸的时间x(大于x),所以UIScrollView内部判断了用户应该是要响应button事件,所以将事件返回给button,那么问题来了,既然事件已经返回给button了,但是为什么没有打印button方法中的 “触发滑动视图上面的UIButton事件” 呢?更重要的是,为何button上移了,说明响应了UIScrollView事件呢?这又和所复写的touchesShouldCancel相关了,因为该方法返回的bool值是true,所以相当于你(UIScrollView)把事件返回给我(button)后,后面又touchesShouldCancel返回true来取消我的响应,真实让我(button)空欢喜一场了!

好,既然你UIScrollView想要我button响应事件,那你后面就不要反悔来取消我的事件啊,那么你就该在touchesShouldCancel中返回false来跟我说:我不取消你的任务了,你做吧!!!于是我们在touchesShouldCancel中将返回值设置成false看看会怎么样,其他不变,然后:

override func touchesShouldCancel(in view: UIView) -> Bool {
    super.touchesShouldCancel(in: view)
    print("Test touches should cancel! current responding view is \(view)")
    return false
}

演示效果:

结果:button没有上移,button方法中的事件被响应,执行了touchesShouldBegin(响应UIScrollView上面的子视图button)和touchesShouldCancel(返回false告诉button,把事件交给button处理)

06 子类化UIScrollView,默认设置为false:操作方法,点击button立即上滑、点击button长按上滑:

import UIKit

class SubScrollView: UIScrollView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.blue
        // 设置为false
        delaysContentTouches = false
    }
    
    override func touchesShouldCancel(in view: UIView) -> Bool {
        super.touchesShouldCancel(in: view)
        print("Test touches should cancel! current responding view is \(view)")
        // 这里返回true,UIScrollView取消子视图的响应
        return true
    }

    override func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?, in view: UIView) -> Bool {
        super.touchesShouldBegin(touches, with: event, in: view)
        print("Test touches should begin! current responding view is \(view)")
        return true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

演示效果:

同样地,从上面我们可以看到,当delaysContentTouches = false时,也就是不延迟内容触摸(优先响应button),但实际情况是,我们看到button上移,button方法中的事件没有被响应,touchesShouldBegintouchesShouldCancel方法被调用,也就是最终响应了UIScrollView事件!!这又是什么样的奇怪现象呢?事实上,尽管你设置了``delaysContentTouches = false, 但是最终决定权还是在UIScrollView的手里,它有一个致命武器就是touchesShouldCancel`返回true来取消你的响应,UIScrollView在告诉你:小子(button),本跟我耍花样,你完全在我的掌握之中,我想让你干你就干,不想让你干你就给我走人!

于是,我们除了设置``delaysContentTouches = false`外,还要:

override func touchesShouldCancel(in view: UIView) -> Bool {
    super.touchesShouldCancel(in: view)
    print("Test touches should cancel! current responding view is \(view)")
    return false
}

现在来看一下效果:

结果:我们看到,当我们直接点击button,和长按button立即上滑时,button不上移,button事件被响应,说明最终并未响应UIScrollView中的时间!但是,我们发现一个奇怪的现象,就是当我点击button立即上滑(演示中的第5、6、7下操作)时,button没有上移,但是button中的方法也没有被调用,说明什么?说明似乎既没有响应button事件,也没有响应UIScrollView中的事件?这个现象就十分奇怪了,事实上,button事件依然被响应,只不过我们别忘记了一点,点击button的模式,现在是.touchUpInside,如果改成touchUpOutside就会响应了,最好的验证方式是将这里得button换成一个UIView,然后给UIView添加一个向上轻扫的手势UISwipeGestureRecognizer,然后就可以验证接收的事件的确是当前UIScrollView的子视图而并非UIScrollView了。

设置成true(默认):

结果:除了立即点击一下button会调用button的方法外,快速点击button上滑,button上移,没有走touchesShouldBegintouchesShouldCancel方法,触发的时间小于延迟触摸内容时间x,所以直接响应UIScrollView滑动;当按下button停留一下上滑时,button依旧上移,但是走了touchesShouldBegintouchesShouldCancel方法,触发的时间大于延迟触摸内容时间x,本应该调用button中的方法,但是由于UIScrollView让touchesShouldCancel返回true取消了button的调用,所以还是走UIScrollView的响应!

设置成false:

结果:我们发现,在将canCancelContentTouches设置成false后,则永远不会调用touchesShouldCancel方法, 当快速点击button上滑或者长按住button直接立即上滑时,button上移,响应了UIScrollView事件,没有走touchesShouldBegintouchesShouldCancel方法, 触摸时间小于延迟内容触摸时间x,所以响应UIScrollView; 当点击button停顿一下后(不松手)继续上滑时,button没有上移,触摸时间大于延迟内容触摸时间x,button事件被响应,走了touchesShouldBegin方法,响应了button点击事件;

例如:

class ViewController: UIViewController, UIScrollViewDelegate {
    
    private var testScrollView: UIScrollView!
    private var testImgView: UIImageView!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        testScrollView = UIScrollView(frame: CGRect(x: 50, y: 264, width: UIScreen.main.bounds.width - 100, height: 200))
        testScrollView.contentSize = CGSize(width: UIScreen.main.bounds.width - 100 , height: 200)
        testScrollView.delegate = self
        testScrollView.backgroundColor = UIColor.orange
        testScrollView.minimumZoomScale = 0.5
        testScrollView.maximumZoomScale = 2
        view.addSubview(testScrollView)
        
        testImgView = UIImageView(frame: testScrollView.bounds)
        testImgView.image = #imageLiteral(resourceName: "testimage2.jpg")
        testScrollView.addSubview(testImgView)
        
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return testImgView
    }
    
    func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {

        let offSetX = scrollView.bounds.width > scrollView.contentSize.width ? (scrollView.bounds.width - scrollView.contentSize.width) * 0.5 : 0.0
        let offSetY = scrollView.bounds.height > scrollView.contentSize.height ? (scrollView.bounds.height - scrollView.contentSize.height) * 0.5 : 0.0
        testImgView.center = CGPoint(x: scrollView.contentSize.width * 0.5 + offSetX, y: scrollView.contentSize.height * 0.5 + offSetY)
    }
}

演示效果:

注意:需要实现缩放效果,代理必须要实现func viewForZooming(in scrollView: UIScrollView) -> UIView?方法,否则无法实现缩放功能,必要时要达到缩放后的一些效果操作还要实现代理的func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)方法!

例如

func tap() {

    // 双击当前图片,使其缩放成原来的0.8倍
    testScrollView.zoomScale = 0.8
    
    // 使图片居中
    let offSetX = testScrollView.bounds.width > testScrollView.contentSize.width ? (testScrollView.bounds.width - testScrollView.contentSize.width) * 0.5 : 0.0
    let offSetY = testScrollView.bounds.height > testScrollView.contentSize.height ? (testScrollView.bounds.height - testScrollView.contentSize.height) * 0.5 : 0.0
    testImgView.center = CGPoint(x: testScrollView.contentSize.width * 0.5 + offSetX, y: testScrollView.contentSize.height * 0.5 + offSetY)
}
public enum UIScrollViewKeyboardDismissMode : Int {

    case none             //  无

    case onDrag       //  拖拽,只要滑动UIScrollView,键盘消失

    case interactive  //  交互式,拖住UIScrollView一直下滑,当接触到键盘时,键盘就跟着同步下滑
}

演示一下interactive

testScrollView.keyboardDismissMode = .interactive
testScrollView.refreshControl = UIRefreshControl()

演示效果

当然,这个属性还可以在菊花下方添加一个title,但是具有一定的透明度,不是那么好用,尝试过很多方法难以改变,字体和颜色是可以设置的,希望后期Apple可以优化,以至于好用!

方法

如果我们需要在某一处触发某个事件就滑动到指定的位置,就可以使用这个方法,十分有用,看下面的例子

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    // testScrollView的frame的height为200, 现在可以使用这个方法滑到最后一页
    testScrollView.setContentOffset(CGPoint(x: 0, y: 200 * 2), animated: true)
}

演示效

解释一下,这个方法中的CGRect中的widthheight必须要设置才能起作用,而且必须大于0!!!否则设置无效! 另外,如果当前区域已经可见,那这个方法什么都不做,什么意思呢?比如现在我所在区域坐标为 (x: 0, y: 0, width: 200, height:200), testScrollView.scrollRectToVisible(CGRect(x: 0, y: 20, width: 100, height: 150), animated: true), 那么这个方法就不会起作用,因为20(y坐标) + 150(height) = 170 < 200 ,当前区域已经包含了scrollRectToVisible所要滑动可见的区域 !

......................................
// 当页面加载成功出现时,滑动条会自动显示出来,停留一下又自动隐藏
// 不设置的话,页面出现时也不会显示滑动条,只有在滑动过程中会显示滑动条
testScrollView.flashScrollIndicators()
view.addSubview(testScrollView)

例如

func doubleTap() {
    testScrollView.setZoomScale(0.5, animated: true)
}

例如:

testScrollView = UIScrollView(frame: CGRect(x: 50, y: 264, width: 300, height: 100))
testScrollView.contentSize = CGSize(width:300 , height: 100)
testScrollView.backgroundColor = UIColor.orange
testScrollView.minimumZoomScale = 0.1
testScrollView.maximumZoomScale = 4
testScrollView.delegate = self
view.addSubview(testScrollView)

func doubleTap() {
    testScrollView.zoom(to: CGRect(x: 0, y: 0, width: 100, height: 50), animated: false)
    print("current zoomScale is \(testScrollView.zoomScale)")
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    print("contentoffset x is \(scrollView.contentOffset.x)")
    print("contentoffset y is \(scrollView.contentOffset.y)")
    print("contentSize: \(scrollView.contentSize)")
}

这个方法使用要注意几点:

1、func zoom(to rect: CGRect, animated: Bool)方法中的CGRect中的x、y不能设置成负数,设置负数的话,默认x、y为0;

2、 zoomScale = min(UIScrollView.width / zoom.width, UIScrollView.height / zoom.height)

3、

contentSize.height = zoomScale * UIScrollView.height
contentSize.width = zoomScale * UIScrollView.width

4、 下面只是相对值,大多数情况下精确,少数情况下有少许误差!

zoom.y * zoomScale >= (UIScrollView.height / zoomScale - zoom.height)
zoom.x * zoomScale >= (UIScrollView.width / zoomScale - zoom.width)

contentOffSet.y = zoom.y * zoomScale - (UIScrollView.height / zoomScale - zoom.height)
contentOffSet.x = zoom.x * zoomScale - (UIScrollView.width / zoomScale - zoom.width)

到这里看得好累有没有??其实我也写得很累,继续搬砖

看一下UIScrollView的UIScrollViewDelegate方法

1 func setContentOffset(_ contentOffset: CGPoint, animated: Bool)

2 func scrollRectToVisible(_ rect: CGRect, animated: Bool)

小结

到此,UIScrollView基本差不多,我们详细讲解了它的一些基本属性和用法,甚至包括一些具体的区别和细节,但是还有一些更深入的底层原理到底怎么样实现的呢?包括它的滑动手势控制,定时器设置,偏移量等还有很多工作需要做,怎么做,还有待研究,有时间会继续深入更新...

</br>

</br>

欢迎加入 iOS(swift)开发互助群:QQ群号:558179558, 相互讨论和学习!

上一篇下一篇

猜你喜欢

热点阅读