raywenderlich翻译系列:UIScrollView教程:起步

253 查看

前言

来源:http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift


UIScrollView是iOS当中最多样和有用的控件之一。它是非常主流的UITableView的基础并且是一种非常好的方式来展现比单屏大的内容。在这篇教程当中你将学习所有关于使用这个控件:

  • 怎么使用UIScrollView来查看一个很大的图片

  • 在缩放中怎么保持UIScrollView的内容是居中的

  • 怎样嵌入一个非常负载的层级进入UIScrollView

  • 怎么样使用UIScrollView的页面特性,和UIPageControl的连接,使得滑动多页面的内容

  • 怎么制作一个”瞥一眼“的UIScrollView,方便快速浏览之前的下页的以及当前页的内容(有点纠结。。。)

  • 还有更多内容

这篇教程假定你已经对Swift和iOS编程有一定的熟悉程度了。如果你是一个完全的新手,你最好先学习一下其他的基础教程。

这篇教程当然也假定你已经知道了怎么使用Interface Builder来添加新的对象到view并且知道连outlets在你的代码和Storyboard(网站这里还拼错了。。)之间。在继续之前你应该要熟悉Storyboards,如果你对Storyboards和Interface Builder很陌生的话最好先浏览一下Storyboard教程。

Getting Started

这一部分基本没有很大翻译的必要,对照网站看图片一步步实现就好了,重点将在下面代码实现和分析的部分

Scrolling and Zooming a Large Image

你现在首要学习的是怎样建立一个scroll view能让用户缩放一个图片并且各种滑动。

首先,你需要建一个view controller。打开ViewController.swift,然后在文件的头部改变类的定义,当然它需要遵循UIScrollViewDelegate协议。

class ViewController: UIViewController, UIScrollViewDelegate {

在类声明内容,添加如下outlet属性

@IBOutlet var scrollView: UIScrollView!

在下一步你将会链接scroll view。

打开storyboard然后拽入一个从 objects library 中拽入View Controller。选择这个新的 view controller 然后在Identity Inspector中,把Class设置为ViewController

这个 view controller 会显示一个 image scroll demo。按住Control键然后从 table view 中的Image Scroll的那一行拽进这个新的 view controller。在显示的弹出目录中,在Selection Segue下选择Push。这样会链接它们因此当用户点击了第一个 row 的时候,这个view controller 会被压入导航栈之中。

从 object library 中拽一个Scroll View完全铺满放入 view controller。

现在是时候来写代码了。打开ViewController.swift然后添加一个新的属性:

var imageView: UIImageView!

这个可以持有用户滑动的图片

现在是时候来到设置 scroll view 最有趣的时候了。用以下代码来替换viewDidLoad之中的代码。

override func viewDidLoad() {
  super.viewDidLoad()
 
  // 1
  let image = UIImage(named: "photo1.png")!
  imageView = UIImageView(image: image)
  imageView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size:image.size)
  scrollView.addSubview(imageView)
 
  // 2
  scrollView.contentSize = image.size
 
  // 3
  var doubleTapRecognizer = UITapGestureRecognizer(target: self, action: "scrollViewDoubleTapped:")
  doubleTapRecognizer.numberOfTapsRequired = 2
  doubleTapRecognizer.numberOfTouchesRequired = 1
  scrollView.addGestureRecognizer(doubleTapRecognizer)
 
  // 4
  let scrollViewFrame = scrollView.frame
  let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
  let scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
  let minScale = min(scaleWidth, scaleHeight);
  scrollView.minimumZoomScale = minScale;
 
  // 5
  scrollView.maximumZoomScale = 1.0
  scrollView.zoomScale = minScale;
 
  // 6
  centerScrollViewContents()
}

这个看起来比较复杂,所以我们一步步来分析。你会看到这个并不是那么可怕。

  1. 首先,你要创建一个名为photo1.png的 image view。然后这里强制解包表示如果工程不能找到photo1.png这张图片程序就会崩溃,这个可以让你更早的发现问题如果你忘记放入图片了!下一步,你要设置 image view 的 frame(它的尺寸和位置)用来把它放在父视图(0,0)位置。最后,把 image view 作为子视图放入 scroll view 当中。

  2. 你必须要告诉 scroll view 内容的 size,当它知道以后就可以得出滑动的横向和纵向距离。在这个例子中,它是图片的尺寸大小。

  3. 这里你需要设置一个双击手势来放大图片。你不需要设置一个UIPinchGestureRecognizer来缩放,因为UIScrollView已经自带了一个!

  4. 接下来,你需要算出 scroll view 的最小缩放比例。1的缩放比例表示内容是正常尺寸,小于1的缩放比例表示内容缩小,大于1的缩放比例表示内容放大。为了得到最小的缩放比例,你要计算出图片基于它的宽度来缩小使得图片能够合适的适配你的 scroll view 的边界。然后基于高度做同样的事情。这两个结果的最小值作为 scroll view 的最小缩放比例。这样的缩放比例可以使得当你完全缩小图片的时候,你可以看到整张图片。

  5. 你设置最大缩放比例为1,因为设置过大的时候可能会导致图片模糊。你设置最初的缩放比例为最小的,这样图片开始就是最小缩放状态。

  6. 这句代码能让 scroll view 居中图片。这个方法在哪里?接下来实现它!

func centerScrollViewContents() {
  let boundsSize = scrollView.bounds.size
  var contentsFrame = imageView.frame
 
  if contentsFrame.size.width < boundsSize.width {
    contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0
  } else {
    contentsFrame.origin.x = 0.0
  }
 
  if contentsFrame.size.height < boundsSize.height {
    contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0
  } else {
    contentsFrame.origin.y = 0.0
  }
 
  imageView.frame = contentsFrame
}

这个方法对于UIScrollView有轻微的作用:如果 scroll view 的contentSize比它的bounds还小,那么就把它放在左上角而不是居中。因为你允许用户完全缩小图片,如果图片放在正中间将会变得非常友好。这个方法实现了把 image view 始终放置在 scroll view 的中间。

最后,把双击点击手势的方法scrollViewDoubleTapped添加进入类。

func scrollViewDoubleTapped(recognizer: UITapGestureRecognizer) {
  // 1        
  let pointInView = recognizer.locationInView(imageView)
 
  // 2
  var newZoomScale = scrollView.zoomScale * 1.5
  newZoomScale = min(newZoomScale, scrollView.maximumZoomScale)
 
  // 3
  let scrollViewSize = scrollView.bounds.size
  let w = scrollViewSize.width / newZoomScale
  let h = scrollViewSize.height / newZoomScale
  let x = pointInView.x - (w / 2.0)
  let y = pointInView.y - (h / 2.0)
 
  let rectToZoomTo = CGRectMake(x, y, w, h);
 
  // 4
  scrollView.zoomToRect(rectToZoomTo, animated: true)
}

这个方法当被双击的时候会触发。接下来就是一步步分析代码:

  1. 首先,你需要得到是在 image view 中的哪里点击的。你将基于这个点来放大图片,你可以想象你是一个用户。

  2. 接下来,你计算1.5倍的缩放比例,但是要和 scroll view 的最大缩放比例做一个比较,取得最小值。

  3. 然后你需要用到步骤1当中的点来计算一个CGRect矩形用来放大。

  4. 最后,你需要告诉 scroll view 去放大,当然你需要一个动画效果来让它感觉起来不错。

获得更多关于iOS手势的信息,浏览相关教程UIGestureRecognizer tutorial

现在,还记得你设置ViewController遵循UIScrollViewDelegate协议吗?好的,现在你需要实现一些必须的代理方法。添加如下方法到类中。

func viewForZoomingInScrollView(scrollView: UIScrollView!) -> UIView! {
  return imageView
}

这个是 scroll view 缩放的核心与灵魂。你要告诉控制器是当你捏合的时候是哪一个 view 需要被放大和缩小。所以,你需要告诉它是你的imageView

最后,添加这个代理方法。

func scrollViewDidZoom(scrollView: UIScrollView!) {
  centerScrollViewContents()
}

这个方法会在用户结束缩放的时候调用。这里,你需要重新居中 view,如果没有,scroll view 不会展现的很自然;相反,可能会偏向左上角。

现在,深呼吸一下,调整一下自己然后编译并运行你的工程!点击Image Scroll如果它显示的很流畅,你可以得到一个好的图片可以让你缩放,捏合和点击!

Scrolling and Zooming a View Hierarchy

如果你不仅仅是在 scroll view 上需要一张图片呢?如果你想要一个复杂的视图层级并且能够缩放和捏合呢?没问题,这里正有一个 scroll view 可以用!好的是在这之前只需要很少的步骤就可以完成了。

创建一个新的文件iOS\Source\Cocoa Touch Class的继承模板。起个类名为CustomScrollViewController然后设置继承于UIViewController。确认Also create XIB file没有被勾选并且语言设置是Swift。点击Next然后保存工程。

打开CustomScrollViewController.swift然后改动如下:

import UIKit
 
class CustomScrollViewController: UIViewController, UIScrollViewDelegate {
  @IBOutlet var scrollView: UIScrollView!
 
}

下一步,打开Main.storyboard然后,增加一个View Controller(从 table view 的第二行用 push segue 的方式连接)。设置它的类为你刚才创建的View Controller

当然,你也需要增加一个Scroll View然后连接一个outlet到它而且还要设置 view controller 作为这个 scroll view 的代理。

然后,打开CustomScrollViewController.swift,添加如下属性:

var containerView: UIView!

与之前的 view controller 不同的地方是不仅没有了UIImageView,你还拥有了一个UIViewcontainerView。这将会成为一个小的指示来表明接下来要发生的事情。

现在,在viewDidLoad里面实现如下:

override func viewDidLoad() {
  super.viewDidLoad()
 
  // Set up the container view to hold your custom view hierarchy
  let containerSize = CGSize(width: 640.0, height: 640.0)
  containerView = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size:containerSize))
  scrollView.addSubview(containerView)
 
  // Set up your custom view hierarchy
  let redView = UIView(frame: CGRect(x: 0, y: 0, width: 640, height: 80))
  redView.backgroundColor = UIColor.redColor();
  containerView.addSubview(redView)
 
  let blueView = UIView(frame: CGRect(x: 0, y: 560, width: 640, height: 80))
  blueView.backgroundColor = UIColor.blueColor();
  containerView.addSubview(blueView)
 
  let greenView = UIView(frame: CGRect(x: 160, y: 160, width: 320, height: 320))
  greenView.backgroundColor = UIColor.greenColor();
  containerView.addSubview(greenView)
 
  let imageView = UIImageView(image: UIImage(named: "slow.png"))
  imageView.center = CGPoint(x: 320, y: 320);
  containerView.addSubview(imageView)
 
  // Tell the scroll view the size of the contents
  scrollView.contentSize = containerSize;
 
  // Set up the minimum & maximum zoom scales
  let scrollViewFrame = scrollView.frame
  let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
  let scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
  let minScale = min(scaleWidth, scaleHeight)
 
  scrollView.minimumZoomScale = minScale
  scrollView.maximumZoomScale = 1.0
  scrollView.zoomScale = 1.0
 
  centerScrollViewContents()
}

viewDidLoad里面设置了一个只有一个根视图的视图层级,而这个根视图就是我们的实例变量,containerView。然后你把这个单视图放在 scroll view 上面。这就是关键-只有一个 view 被加到 scroll view 上面如果你准备缩小它,因为在代理回调viewForZoomingInScrollView中,只能返回一个 view。你设置了zoomScale为1,而不是minScale所以 content view 是正常尺寸,而不是铺满屏幕。

再一次的,实现centerScrollViewContents方法和两个UIScrollViewDelegate方法,只需要把以前的方法复制过来,替换imageViewcontainerView

Note:你可以意识到缺少了`UITapGestureRecognizer `。这对于这篇教程来说很简单。作为额外练习轻松就可以添加进去了。

现在编译然后运行你的工程。这一次,选择Custom View Scroll然后看看它的精彩表现,因为你能捏合和缩放一个不错的动手编写的 UIView 场景。

Paging with UIScrollView

在教程中的第三部分,你要创建一个可以页面滑动的 scroll view。这意味着当你停止扎ui时候 scroll view 会停在一页上。在 APP Store 上面当你浏览一个 app 的快照的时候看到的效果就是这样的。

创建一个新的子类模板iOS\Source\Cocoa Touch Class。命名为PagedScrollViewController然后让它继承自UIViewController。确保Also create XIB file不被勾选,然后选择语言为Swift。点击下一步然后保存工程。

打开PagedScrollViewController.swift然后改成如下的代码:

import UIKit
 
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
  @IBOutlet var scrollView: UIScrollView!
  @IBOutlet var pageControl: UIPageControl!
}

然后,去Main.storyboard 中,选择 table view 当中第三行链接到 View Controller。然后如同之前一样,设置它的类为PagedScrollViewController

把主视图的背景颜色设置为黑色,因此当 page control 添加的时候能看得见。默认是白色的,白色的东西放在白色是无用的!

添加一个 Page Control元素到 view 的下面然后设置它的宽等于屏幕的宽。然后链接 outlet 到pageControl

当然,也要链接Scroll View,然后设置它的代理为 view controller,就像之前做的一样。调整Scroll View的尺寸铺满 view controller,但是留一些空间给Page Control

然后在Attributes Inspector里面勾选Paging Enabled

现在打开PagedScrollViewController.swift然后添加以下属性:

var pageImages: [UIImage] = []
var pageViews: [UIImageView?] = []

现在你可能发现有些不同了。没有容器视图,只有两个数组。

  • pageImages:放置所有的需要展示的图片

  • pageViews:放置UIImageView的实例而它们是用来显示在它们各自的 page 上面的图片。这是个可选类型,因为你需要懒加载这些 pages 所以你可能要处理数组里面的 nil 情况。

接下来,完成viewDidLoad

override func viewDidLoad() {
  super.viewDidLoad()
 
  // 1
  pageImages = [UIImage(named: "photo1.png")!,
    UIImage(named: "photo2.png")!,
    UIImage(named: "photo3.png")!,
    UIImage(named: "photo4.png")!,
    UIImage(named: "photo5.png")!]
 
  let pageCount = pageImages.count
 
  // 2
  pageControl.currentPage = 0
  pageControl.numberOfPages = pageCount
 
  // 3
  for _ in 0..<pageCount {
    pageViews.append(nil)
  }
 
  // 4
  let pagesScrollViewSize = scrollView.frame.size
  scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(pageImages.count),
                                  height: pagesScrollViewSize.height)
 
  // 5
  loadVisiblePages()
}

我们来一步一步的分析所发生的事情:

  1. 首先,设置page images。你需要添加五个图片进去然后这个数组来接受他们。

  2. page的索引从0开始,所以你设置最初的page然后告诉它有几个 pages。

  3. 接下来,设置放置了UIImageView实例的数组。首先,没有 pages 被懒加载所以你需要放置 nil 对象进去 一一对应。接着,你需要解包 option 来检查 page 是否被载入。

  4. 这个scroll view,如同之前的一样,需要知道它的content size。因为你像横向滑动(如果你想的话垂直方向更容易),计算出的宽式等于 pages 的个数来乘以scroll view的宽度。高度是和scroll view的高度等高。

  5. 初始状态需要设置一些 pages,所以执行loadVisiblePages

添加如下方法:

func loadPage(page: Int) {
  if page < 0 || page >= pageImages.count {
    // If it's outside the range of what you have to display, then do nothing
    return
  }
 
  // 1
  if let pageView = pageViews[page] {
    // Do nothing. The view is already loaded.
  } else {
    // 2
    var frame = scrollView.bounds
    frame.origin.x = frame.size.width * CGFloat(page)
    frame.origin.y = 0.0
 
    // 3
    let newPageView = UIImageView(image: pageImages[page])
    newPageView.contentMode = .ScaleAspectFit
    newPageView.frame = frame
    scrollView.addSubview(newPageView)
 
    // 4
    pageViews[page] = newPageView
  }
}

记住每个page都是一个存放在可选数组的UIImageView。当view controller载入后,数组是空的。这个方法用来载入每个页面的内容。

  1. 首先,你使用了拆包来检查是否载入了试图。如果pageView已经包含了UIImageView的话那就什么都不做并忽略后面的代码。

  2. 如果pageView是空的,就需要创建一个page。所以首先计算出这个pageframe。它和scroll view的尺寸一样,坐标y轴offset为0,然后x轴offset为一页的宽乘以page的数量。

  3. 创建一个新的UIImageView,设置一下然后加入scroll view

  4. 最后,把刚才创建的视图放入数组来替换掉空值,因此当下次载入的时候,就不会执行 if statement语句中,因为page的视图已经被创建了。

接下来,添加下列方法:

func purgePage(page: Int) {
  if page < 0 || page >= pageImages.count {
    // If it's outside the range of what you have to display, then do nothing
    return
  }
 
  // Remove a page from the scroll view and reset the container array
  if let pageView = pageViews[page] {
    pageView.removeFromSuperview()
    pageViews[page] = nil
  }
}

这个方法是来清楚之前通过loadPage()方法创建的page。首先先检查pageViews数组中的这个page是不是空的。如果不为空,从scroll view当中移除这个视图 然后更新数组中的值为 nil,表现出这个page不再存在。

为什么要使用懒加载和移除视图,你可能要问?好吧,在这个例子中,它可能在开始载入所有pages的时候不会有问题,因为这里只有仅仅5个视图还不足以把整个内存给吃掉。想象意向如果你有100张图并且每个5MB大小。这就会吃掉500MB的内润如果一次加载所有的pages!你的 app 会快速占用可用的内存然后被系统给杀掉。懒加载意味着你只需要加载一定数量的pages在内存中,任何时候都可以。

上面定义的两个方法是连在一起的都是通过loadVisiblePages()来调用的。添加以下的代码实现:

func loadVisiblePages() {
  // First, determine which page is currently visible
  let pageWidth = scrollView.frame.size.width
  let page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))
 
  // Update the page control
  pageControl.currentPage = page
 
  // Work out which pages you want to load
  let firstPage = page - 1
  let lastPage = page + 1
 
  // Purge anything before the first page
  for var index = 0; index < firstPage; ++index {
    purgePage(index)
  }
 
  // Load pages in our range
  for index in firstPage...lastPage {
    loadPage(index)
  }
 
  // Purge anything after the last page
  for var index = lastPage+1; index < pageImages.count; ++index {
    purgePage(index)
  }
}

这里就获得了现在正在scroll view上的page,更新page control并且载入或者清除相关的pages。计算过程可能开起来很吓人,但是这还不错。你可以通过放入一些数字在i。(注意floor()方法会产生一些小数接近最小的整数)。

你选择载入当前的页面和它周围的页面。当一个用户开始滑动的时候,它可以看到下一张准备到中间的页面。你也可以载入之前的或者下两张或下三张的图片,但是这个会增加内容使用并且没什么用。

最后一件事情就是实现UIScrollView的代理方法。这次你只需要实现scrollViewDidScroll()的代理方法。添加进PagedScrollViewController.swift

func scrollViewDidScroll(scrollView: UIScrollView!) {
  // Load the pages that are now on screen
  loadVisiblePages()
}

这些做完以后就能保证当滑动的时候,相关的pages能被载入(不必要的会被清除)。

编译并运行工程,看看scroll view的翻页功能。

总结

UIScrollView是一个很重要的控件,后面的UITableView也是继承自它。所以当我们足够了解UIScrollView以后,对于UITableView我们才能更加方便快捷的使用。

raywenderlich的文章非常适合大家学习,因为文章都是在面向大众的教程,顺序逻辑思维,逐步带入的教学方式,很适合大家观看。

当然,这其中的翻译难免有不妥当的地方,还请大家多多的指教,有什么建议和意见在下面的评论回复,谢谢!

(最近更新时间2015-08-27)

本系列翻译属于个人兴趣,不作任何其他用途,如有任何版权信息问题,请联络本人做相关处理,谢谢!
感谢原文来源网站:www.raywenderlich.com