2017-07-22
作者:Owen Brown,原文链接,原文日期:2017-07-21
译者:Chenghui Bai;校对:Chenghui Bai;定稿:Chenghui Bai
学习如何使用UISCrollView进行分页,缩放,滚动,等等。。。!
更新日志:我们用Xcode9.0和Swift4.0对这个教程进行了更新。原著是Ray Wenderlich.
UIScrollView 在iOS中一个很常用的控件.是 UITableView 的基类,使用它展示超过一个屏幕内容是很好的方法。在这个 UIScrollView 教程中,将建立一个类似相册的app,并且将学习到关于UIScrollView 的一切。
你讲学会:
.如何使用UIScrollView 去缩放并查看一张很大的图片。
.如何保证以UIScrollView 的内容为中心进行缩放。
.如何使用UIScrollView自动布局进行竖直方向滚动。
.如何保证当输入控件是可见的(不会被键盘挡住),当键盘弹出来的时候。
.如何使用UIPageViewController去实现多个页面内容的滚动。
本教程假设你已经知道如何使用 Interface Builder 添加一个对象,通过代码和storyboard连线。如果你还不熟悉怎么操作,可以参考之前的教程: Storyboards tutorial.
入门指南
点击 here 为 UIScrollView 教程下载启动项目,打开Xcode.编译然后运行看看是啥样:
你选择一张照片去看他的全部尺寸,很遗憾,你不能看见整张图片由于设备尺寸的限制。如果你真的想像相册app那样在屏幕的默认
尺寸下吧整张图片呈现,并缩放去看细节,能做到吗?是的,你可以。
滚动和缩放一张大图
揭开本 UIScrollView 课程序幕,你将创建一个scrollview让用户可以对图像进行缩放和平移。
打开Main.storyboard,从右下角的Document Outline 中拖拽一个scrollView到Zoomed Photo View Controller,然后,拖一个imageView到scrollView中 ,你的Document Outline 现在应该是下面这样:
看见红色的点了吗?这是Xcode编译时告诉你对不符合Auto Layout的规则。
去解决它,选中scrollview并且点击位于storyboard 窗口底部的大头针按钮,添加上下左右的约束,设置约束值为0,不要有间距。像下面这样:
现在,选中imageView,同样给他添加约束。
之后,如果你发现一个Auto Layout的警告,就在Document Outline 选中 Zoomed Photo View Controller 然后选择编辑或解决约束问题,或更新frame,如果你没有发现警告,Xcode可能已经为你自动更新了frame,因此你什么也不需要做,直接编译运行。
感谢scrollView,你现在可以通过滑动看全尺寸图片,但是如果你想看到图片缩放来使用屏幕,或者想看到放大缩小?
你需要为他写代码!
打开 ZoomedPhotoViewController.swift,在类的申明中添加:
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
回到 Main.storyboard, 设置scrollView的outlet到scrollView,并且设置代码到Zoomed View Controller. 同样,链接新的约束的outlets与 Document Outline 的约束相关联。像这样:
回到ZoomedPhotoViewController.swift, 在文件接尾添加如下代码:
extension ZoomedPhotoViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
这让 ZoomedPhotoViewController 遵循UIScrollViewDeleate 并且实现viewForZooming(in:) 方法。这个方法在告诉scrollView的哪个子视图需要捏合缩放,在这告诉需要缩放的是imageview.
下一步,在viewDidLoad():方法中添加如下代码:
fileprivate func updateMinZoomScaleForSize(_ size: CGSize) {
let widthScale = size.width / imageView.bounds.width
let heightScale = size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
}
这个方法为scrollView计算缩放scale.当scale为1表示内容按正常尺寸显示。当scale小于1内容缩小显示,当scale大于1内容放大显示。
为了获得最小缩放比scale,首先计算imageview紧贴scrollView所需的宽度比,然后计算高度比,然后在scrollView中取他们中最小值作为缩放比,这样,你将会看见图片完全放大。
由于 maximumZoomScale 默认为1,你不需要设置他,如果你设置大于1,放大图片可能会模糊,如果设置小于1,将放大不到图片本身的分辨率。
最后,在每次控制器更新子视图时,你需要更新最小缩放比。在前面的方法之前,添加下面代码:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateMinZoomScaleForSize(view.bounds.size)
}
编译运行,结果应该和下面一样:
现在你可以捏合缩放了,并且图片最初是适配屏幕的。真棒!
然而,现在还有一个问题:图像被固定到滚动视图的顶部,如果是根据中心点固定会更好,对吧?
还是在ZoomedPhotoViewController.swift,在viewForZooming(in:)方法后面添加代码:
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateConstraintsForSize(view.bounds.size)
}
fileprivate func updateConstraintsForSize(_ size: CGSize) {
let yOffset = max(0, (size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
view.layoutIfNeeded()
}
用户每次滚动scrollView都会调用scrollViewDidZoom方法,像应的,会调用你的updateConstraintsForSize(_:) 方法通过传view的size.
updateConstraintsForSize(_:) 在scrollView中比较麻烦的是:如果滚动视图的内容尺寸小于边界,内容放在左上而不是中心。
在这适配imageview的布局约束。你计算图像垂直方向的第一个中心点是从view的高度减去ImageView的高度,然后除以2.这个值作为imageview的top和bottom的外间距约束。相似的,您可以计算一个左右的offset去约束imageView的宽度。
给自己一点表扬,编译运行你的项目!选一个图片,如果一切顺利的话,你会得到一个可爱的图像,你可以缩放和平移。
竖直方向滚动
现在假设你想改变photoscroll显示图像的顶部添加留言吧。取决于评论的长度,您可能会得到比您的设备所能显示的更多的文本:ScrollView可以帮你做到!
注意:通常情况下,Auto Layout 考虑上下左右距离一个视图的间距,而 UIScrollView 通过改变他的原始bounds来滚动他的内容。使用 Auto Layout,scrollVIew的边实际上指的是contentView的边缘。
用自动布局来调整滚动视图的frame,有关scrollView的宽度和高度的约束必须明确,否则scrollView的边缘必须绑定到其子树之外的视图。
在这 this technical note 你可以知道的更多。
下面你将学习怎么使用Auto Layout 来 fix scrollView的width,实际上他是contentSize的width.
Scroll View 和 Auto Layout
打开Main.storyboard 布局一个页面。
首先,添加一个View Controller.在Simulated Size,用Freeform替换掉Fixed,宽高设置为 340 * 800.
你会注意到这个控制器又窄又长,模拟一个竖直方向很长的内容。这样模拟有助于你排列Interface Builder.他没有运行时效果。
在最新创建的控制器中的 Attributes Inspector 取消勾选Adjust Scroll View Insets。
添加一个scrollView,填充这个控制器。
设置左右约束为0,并取消勾选 Constrain to margin.设置上下约束也为0 。
添加一个view作为scrollView的子视图,并填充整个scrollView.然后重新命名为 Container View. 和之前一样,设置上下左右约束为0,并取消勾选 Constrain to Margins.
为了修复Auto Layout 的错误,你需要设置scrollView的size.设置Container View 的宽度和控制器宽度相等。Container View 的高度约束设置为500.
注意:Auto Layout rules 有一条规则就是scrollView的contentSize必须确定。这是使用Auto Layout 设置scrollView size很关键的一步。
添加一个imageView到 Container View.
在 Attributes Inspector 中,为imageView设置图片名为phone1的图片,选择 Aspect Fit模式,并勾选clips to bounds.
和之前一样,添加上左右约束,高度约束设置为300.
在imageView下面添加一个Lable视图,设置文本内容为“What name fits me best?”添加相对Container View的水平居中的约束。设置相对Photo View竖直方向间距为0的约束。
在Container View中的Lable下面添加一个TextField视图,左右间距相对Container View的约束为8。竖直方向相对Lable约束为30.
下一步,你需要连线一个segue 到最新创建的控制器。
到这,首先删除 Photo Scroll与Zoomed Photo View Controller 之间已存在的segue,不要当心,在控制器上所做的所有工作稍后将被添加到您的应用程序中。
在Photo Scroll 页中,从PhotoCell 拖线连接到最新的控制器,添加一个segue,并设置identifier为showPhotoPage。
编译运行。
你可以看见,竖直方向上布局是正确的,你尝试选择屏幕。横屏,没有足够的空间来显示所有内容,不过scrollVIew允许你适当的滚动去看lable和textfield。不幸的是,新视图控制器中的image是硬编码的,在集合中选择的这张image他不显示。
fix,在segue执行完成你需要传递imageName到控制器。
在iOS\Source\Cocoa Touch Class 创建一个新的文件,命名为PhotoCommentViewController, 他是UIViewController的子类。确保创建的文件是swift的。点击下一步,保存他到项目中。
用下面的代码替换掉PhotoCommentViewController.swift中的内容:
import UIKit
class PhotoCommentViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var nameTextField: UITextField!
var photoName: String?
override func viewDidLoad() {
super.viewDidLoad()
if let photoName = photoName {
self.imageView.image = UIImage(named: photoName)
}
}
}
用phoneName创建image给imageView设置image.
回到storyboard ,打开控制器的 Identity Inspector ,选择Class栏目设置为PhotoCommentViewController,然后连线scrollView,imageView和 Textfield。
打开CollectionViewController.swift, 用下面的代码替换prepare(segue:sender:) :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let cell = sender as? UICollectionViewCell,
let indexPath = collectionView?.indexPath(for: cell),
let photoCommentViewController = segue.destination as? PhotoCommentViewController {
photoCommentViewController.photoName = "photo\(indexPath.row + 1)"
}
}
这是当点击一个相片去设置相片名显示的PhotoCommentViewController。
编译运行:
当需要滚动去查看更多时,你的视图排列内容的很好。现在你将意识到键盘的2个问题:第一,当输入文字时,键盘挡住了你的textfield.第二,你没办法取消键盘。马上解决这个问题。
管理键盘
不像UITableViewController,他能通过滚动内容的方式去自动处理键盘,当你直接使用UIScrollView时,需要你手动去管理键盘。
你可以通过PhotoCommentViewController监听键盘发送显示和隐藏的通知。打开PhotoCommentViewController.swift
, 在viewDidLoad()中添加下面的代码(暂时忽略编译报错):
NotificationCenter.default.addObserver(
self,
selector: #selector(PhotoCommentViewController.keyboardWillShow(_:)),
name: Notification.Name.UIKeyboardWillShow,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(PhotoCommentViewController.keyboardWillHide(_:)),
name: Notification.Name.UIKeyboardWillHide,
object: nil
)
下一步,在对象销毁时,添加下面的方法去移除通知的监听者。
deinit {
NotificationCenter.default.removeObserver(self)
}
然后,然后将上面所说的方法添加到视图控制器中。
func adjustInsetForKeyboardShow(_ show: Bool, notification: Notification) {
let userInfo = notification.userInfo ?? [:]
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let adjustmentHeight = (keyboardFrame.height + 20) * (show ? 1 : -1)
scrollView.contentInset.bottom += adjustmentHeight
scrollView.scrollIndicatorInsets.bottom += adjustmentHeight
}
@objc func keyboardWillShow(_ notification: Notification) {
adjustInsetForKeyboardShow(true, notification: notification)
}
@objc func keyboardWillHide(_ notification: Notification) {
adjustInsetForKeyboardShow(false, notification: notification)
}
在adjustInsetForKeyboardShow(_:,notification:)中,获取键盘高度,并拿键盘高度加上一个20个点的高度作为scrollView的contentInset.这样,上下滑动UIScrollView,UITextField页会一直在屏幕上可见。
当通知被取消时,要么keyboardWillShow(:)方法被调用 ,要么keyboardWillHide(:) 方法被调用。这些方法都会调用 adjustInsetForKeyboardShow(_:,notification:), 指示scrollView移动的方向。
隐藏键盘
在 PhotoCommentViewController.swift:中添加方法:
@IBAction func hideKeyboard(_ sender: AnyObject) {
nameTextField.endEditing(true)
}
这个方法将会取消掉textfield的第一响应者状态。这样会隐藏键盘。
最后,打开Main.storyboard,在Object Library 中拖一个点击手势到Photo Comment View Controller 的view上,然后在Photo Comment View Controller中写一个IBAction类型的方法hideKeyboard(_:) 。
为了更友好的交互 ,当点击键盘的return按钮时页应该隐藏键盘。右击nameTextField,然后连线 Primary Action Triggered到hideKeyboard(_:)方法。
编译运行:
到 Photo Comment View Controller 页,点击text field,然后点击控制器的view的任何地方,键盘应正确显示和隐藏自己相对于屏幕上的其他内容。点击return按钮时页应该一样。
利用UIPageViewCpontroller实现分页
这是UIScrollView教程的第三部分,你讲创建一个允许分页的scrollView。这意味着当你拖拽结束后他会锁定到某一页。这个动作你可以在app store中看一个app的截图时看见。
打开 Main.storyboard,从Object Library拖拽一个Page View Controller ,打开 Identity Inspector,进入 PageViewController.
在Attributes Inspector中,默认Transition Style 被设置为Page Curl ,我们改为Scroll,并设置 Page Spacing为8。
在Photo Comment View Controller 的 Identity Inspector
中,为PhotoCommentViewController设置一个Storyboard ID ,这样你就可以从代码中引用它了。打开PhotoCommentViewController.swift,添加属性:
var photoIndex: Int!
这将引用要显示的照片的索引,并将被page view controller使用。
在iOS\Source\Cocoa Touch Class创建一个文件,命名ManagePageViewController,是UIPageViewController的子类。
打开 ManagePageViewController.swift
,用下面的代码替换里面的内容:
import UIKit
class ManagePageViewController: UIPageViewController {
var photos = ["photo1", "photo2", "photo3", "photo4", "photo5"]
var currentIndex: Int!
override func viewDidLoad() {
super.viewDidLoad()
// 1
if let viewController = viewPhotoCommentController(currentIndex ?? 0) {
let viewControllers = [viewController]
// 2
setViewControllers(
viewControllers,
direction: .forward,
animated: false,
completion: nil
)
}
}
func viewPhotoCommentController(_ index: Int) -> PhotoCommentViewController? {
guard let storyboard = storyboard,
let page = storyboard.instantiateViewController(withIdentifier: "PhotoCommentViewController")
as? PhotoCommentViewController else {
return nil
}
page.photoName = photos[index]
page.photoIndex = index
return page
}
}
下面是这代码做了什么:
1.viewPhotoCommentController(:) 是通过storyboard创建一个 PhotoCommentViewController类型的实例。你通过一个imageName做为参数,显示的视图与您在前一屏中选择的图像匹配。
2.通过一个包含一个控制器的数组作为参数创建UIPageViewController。
下面,你需要实现UIPageViewControllerDataSource。添加下面的类扩展代码:
extension ManagePageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let viewController = viewController as? PhotoCommentViewController,
let index = viewController.photoIndex,
index > 0 {
return viewPhotoCommentController(index - 1)
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let viewController = viewController as? PhotoCommentViewController,
let index = viewController.photoIndex,
(index + 1) < photos.count {
return viewPhotoCommentController(index + 1)
}
return nil
}
}
当page在改变时 UIPageViewControllerDataSource 允许你提供内容。在向前和向后的方向上提供分页控制器实例。在这两种情况下,photoindex是用来确定哪些图像当前显示。viewController作为参数,表示当前基于 photoIndex 显示的控制器,一个新的控制器被创建并返回。
你依然需要设置dataSource .在 viewDidLoad()
添加下面代码:
dataSource = self
要使page view 运行只剩下几件事情要做才能。首先,您将修复应用程序的流程。
回到 Main.storyboard ,选择Page View Controller ,在Identity Inspector, 设置class为ManagePageViewController。
移除之前创建的showPhotoPage 。然后从 Scroll View Controller的Photo Cell 上拖拽到 Manage Page View Controller,并设置segue。在Attributes Inspector 设置segue。和之前一样指定名称为showPhotoPage。
打开 CollectionViewController.swift 改变实现 prepare(segue:sender:) 方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let cell = sender as? UICollectionViewCell,
let indexPath = collectionView?.indexPath(for: cell),
let managePageViewController = segue.destination as? ManagePageViewController {
managePageViewController.photos = photos
managePageViewController.currentIndex = indexPath.row
}
}
编译运行:
现在,你可以在不同的详情页面之间滑动。
显示一个page control indicator
在UIScrollView教程的最后一个部分,你将为你的应用添加一个UIPageControl。
很幸运,UIPageViewController 有能力自己提供一个UIPageControl。
这样,你的UIPageViewController 必须有一个transition style 是 UIPageViewControllerTransitionStyleScroll,并且你必须实现2个指定的UIPageViewControllerDataSource数据源方法。你之前设置 Transition Style 做的很好。因此你现在只需要在的ManagePageViewController extension实现2个UIPageViewControllerDataSource数据源方法:
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return photos.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return currentIndex ?? 0
}
在 presentationCount(for:)中,指定在 page view controller总共显示多少页。
在 presentationIndex(for:)中,当前显示了第几页。
然后实现要求实现的代理方法,你可以添加更多的定制与uiappearance 相关 API。在AppDelegate.swift,用下面的代码替换application(application: didFinishLaunchingWithOptions:):
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let pageControl = UIPageControl.appearance()
pageControl.pageIndicatorTintColor = UIColor.lightGray
pageControl.currentPageIndicatorTintColor = UIColor.red
return true
}
这样可以自定义 UIPageControl.的颜色。
编译运行:
把所有的放一起
在最后一步,是去添加当图像被点击进到的缩放视图。
打开PhotoCommentViewController.swift,添加下面的代码:
@IBAction func openZoomingController(_ sender: AnyObject) {
self.performSegue(withIdentifier: "zooming", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
if let id = segue.identifier,
let zoomedPhotoViewController = segue.destination as? ZoomedPhotoViewController,
id == "zooming" {
zoomedPhotoViewController.photoName = photoName
}
}
在 Main.storyboard, 在 Photo Comment View Controller和 Zoomed Photo View Controller.之间添加一个Show Detail .选中这个新的segue,打开 Identity Inspector 并设置Identifier 为 zooming.
在 Photo Comment View Controller 中选中imageView。打开Attributes Inspector 选中 User Interaction Enabled。 从imageView 上拖一个手势 Tap Gesture Recognizer ,响应方法为openZoomingController(_:).
现在,你在 Photo Comment View Controller 中点击一个图片,你将进入 Zoomed Photo View Controller ,在 Zoomed Photo View Controller 中你可以缩放图片。
最后一次,编译运行:
Yes!你做到了!你制作了一个相册app,你可以通过切换查看一个集合的图片。也具备缩放内容的能力。
Where to Go From Here?
Here is the final PhotoScroll project with all of the code from this UIScrollViewtutorial.
You’ve delved into many of the interesting things that a scroll view is capable of. If you want to go further, there is an entire video series dedicated to scroll views. Take a look.
Now go make some awesome apps, safe in the knowledge that you’ve got mad scroll view skillz!
If you run into any problems along the way or want to leave feedback about what you've read here, join the discussion in the comments below.