Swift 程式語言

UIScrollView 滾動視圖初學者指南

UIScrollView 滾動視圖初學者指南
UIScrollView 滾動視圖初學者指南
In: Swift 程式語言

在iOS中,滾動視圖(scroll view)是用來瀏覽無法在整個畫面容下的其他內容。滾動視圖有兩個主要用途:

  • 提供使用者拖曳至他們想要呈現的內容區域
  • 提供使用者使用手指縮放手勢來對所呈現的內容放大或縮小

在iOS App的常見控制 – UITableView – 是一個UIScrollView的子類別,提供了一個可以檢視視圖內容大於本身畫面的一個很棒的方式。

本篇教學中,我們會來看各種UIScrollView的觀念、其中包括以程式建立一個滾動視圖與介面建構器(Interface Builder)、滾動(scrolling)與縮放(zooming)、以及巢狀滾動視圖(nested scroll views)。

UIScrollView Guide

往下閱讀之前,先 下載這個專案的原始檔案, 其中包括了我們要在這篇教學中使用的所有檔案。

以程式建立滾動視圖

滾動視圖就跟其他視圖的建立方式一樣,不是以程式來建立就是在介面建構器中建立,只要稍微設置一下,就可以完成滾動的功能。

滾動視圖就跟其他視圖一樣是以插進去一個控制器或視圖階層(view hierarchy)中來建立。只要兩個步驟就可以完成滾動視圖的設置:

  • 你必須要設定contentSize 屬性為滾動內容的大小。這裏指定滾動區域的大小。
  • Y你必須加入可以顯示以及可以讓滾動視圖滾動的視圖。這些視圖主要作為內容的呈現。

你可以幫你的程式任意設置視覺上的提示,像是垂直與水平滾動指示器、拖曳反彈以及滾動的方向限制。

我們會以程式來建立滾動視圖來開始。從你下載的專案檔案打開ScrollViewDemo專案。裡面包含了一個簡單專案,其中在Storyboard加上一個single view controller,並連結上專案中所建立的ScrollViewController 類別。我也加入了一張我們待會會用到名稱為 image.png的圖像((照片由 unsplash.com所提供).

打開 ScrollViewController.swift,並加入以下的屬性。

var scrollView: UIScrollView!
var imageView: UIImageView!

修改 viewDidLoad() 如下

override func viewDidLoad() {
    super.viewDidLoad()
        
    imageView = UIImageView(image: UIImage(named: "image.png"))
        
    scrollView = UIScrollView(frame: view.bounds)
    scrollView.backgroundColor = UIColor.blackColor()
    scrollView.contentSize = imageView.bounds.size
    scrollView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        
    scrollView.addSubview(imageView)
    view.addSubview(scrollView)
}

上面的程式建立了一個滾動視圖與一個圖像視圖。圖像視圖設為滾動視圖的子視圖。 contentSize 設定滾動區域的大小。我們設定跟圖像視圖(2000×1500)大小相同。我們設定跟圖像視圖(2000×1500)大小相同。我們也設定滾動視圖的背景顏色為黑色,所以圖像會有個黑色背景,我們設定滾動視圖的 autoresizingMask.FlexibleWidth.FlexibleHeight所以當裝置旋轉時,會重新調整大小。執行這個App,你應該能夠滾動並看見圖片的其他部分。

scroll view demo #1

當你執行App,你可能會注意到顯示的部分圖像是原來圖片的左上角。

scroll view demo #2

這是因為滾動視圖邊界原點設為(0,0),也就是左上角,如果你想要變更App啟動後圖像內容顯示的位置,你必須變更視圖的邊界原點,因為設定這個位置在處理滾動視圖時很常見,UIScrollView有一個 contentOffset 屬性,可以設定跟改變邊界原點一樣效果。

將以下的陳述貼至該行後面,設定滾動視圖的 autoresizingMask.

scrollView.contentOffset = CGPoint(x: 1000, y: 450)

再次執行App,你會見到滾動視圖,已經移動並顯示照片其他的部分。所以你可以決定當視圖載入後,所要呈現的部分。

scroll view image demok

縮放

我們已經加入滾動視圖,可以讓使用者滾動瀏覽超過畫面大小的圖片部分。這很棒,但是如果使用者可以放大縮小這會更有用。

想要支援縮放功能,你必須為你的滾動視圖設定一個代理(delegate)。 這個代理物件必須遵循 UIScrollViewDelegate 協定(protocol)。該代理類別必須實作 viewForZoomingInScrollView() 方法並回傳要縮放的視圖。

你也必須要指定使用者可以縮放的量。只要設定滾動視圖的minimumZoomScalemaximumZoomScale 的屬性值。兩者預設值皆為1.0。

修改 ScrollViewController 類別定義如下所示。

class ScrollViewController: UIViewController, UIScrollViewDelegate {

然後加入以下的函式至類別。

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

然後在 viewDidLoad()底下加入以下幾行。

scrollView.delegate = self	
scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0

在以上的程式中我們設定 zoomScale 為 1.0 並指定最小以及最大的縮放因子。 執行這個App,一開始會有相同的縮放因子,如前所示(zoomScale of 1.0)。當你以手指縮放視圖,便可以將其往上或往下滾動至它的最大以及最小縮放因子。 我們設定 maximumZoomScale 為4.0,maximumZoomScale, 因此你可以將圖片放大四倍, 不過結果並不是太好,因為尺寸比原來大上許多,所以變得有點模糊。我們在下一步將其變更回原來1.0的預設值。

image03

以上,我們設定 minimumZoomScale 為 0.1, 結果圖片變得非常小,畫面上留下了許多空白。在橫向模式,圖片旁空白的區域變得更大。我們想要讓這個圖片「縱橫比相符並顯示(Aspect Fit)」在滾動視圖中,所以它可以在顯示全圖像的情況下盡可能佔滿較多的空間。

image04

要這麼做的話,我們將會使用滾動視圖與圖像視圖比來計算最小縮放因子。

首先從viewDidLoad()移除以下三個陳述。

scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0

加入以下的函式至類別中,我們取得寬度與高度比,並挑選最小的值,設定它為最小縮放比,注意我已經移除maximumZoomScale的設定,所以它被設定為預設值1.0。

func setZoomScale() {
    let imageViewSize = imageView.bounds.size
    let scrollViewSize = scrollView.bounds.size
    let widthScale = scrollViewSize.width / imageViewSize.width
    let heightScale = scrollViewSize.height / imageViewSize.height
        
    scrollView.minimumZoomScale = min(widthScale, heightScale)
    scrollView.zoomScale = 1.0
}

然後在 viewDidLoad()底部呼叫這個函式

setZoomScale()

同樣的,加入以下這段程式,這可以讓使用者將裝置轉向後,做縮放時圖像會跟著調整比例。

override func viewWillLayoutSubviews() {
    setZoomScale()
}

執行這個App,現在當你將圖像縮小,圖像會佈滿畫面較多的空間,並顯示全部畫面。

image05

從上面的圖片你可以注意到,滑動視圖的內容,也就是圖片本身,位於畫面的左上方,我們要把它置於畫面中心。

加入以下的函式至類別中。

func scrollViewDidZoom(scrollView: UIScrollView) {
    let imageViewSize = imageView.frame.size
    let scrollViewSize = scrollView.bounds.size
        
    let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
    let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
        
    scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
}

這個函式會在每次縮放後被呼叫。它告訴代理滑動視圖的縮放因子已經變更。在以上的程式中,我們計算會應用在圖片周邊的padding/inset 來將其置中。對於上方與底部的值,我們檢查圖像的高度是否小於滾動視圖的高度,如果是的話是設定其留白(padding)值為兩個視圖差的一半,否則的話就設定其值為0。針對水平留白部分也做同樣的處理,然後我們設定視圖的 contentInset。這是內容視圖插入後與滾動視圖周邊的距離。

執行這個App,現在當你縮至最小比例時,內容應該已經能夠置中了。

image06

按下做縮放

基本的 UIScrollView 類別只要加上一點程式就可以支援手指縮放手勢(pinch in與pinch out)。但是如果要使用偵測手指按下手勢來支援更豐富的縮放體驗,則需要多一點的工作才能完成。

iOS使用者介面指南(iOS Human Interface Guidlines)定義了點兩下(double-tap)來縮放。不過這有些條件:就是視圖單一方向的縮放(像是照片App 一樣,在相片上按兩下會放大至最大比例,然後再按兩下則縮回最小比例),或者連續點擊會放到最大,達到之後則會回到全螢幕視圖。但是一些應用在面對按下縮放功能時需要更多彈性的處理方式,一個例子是地圖應用程式。地圖App支援點兩下放大,再按兩下放更大,如果要縮小,則使用兩隻手指靠在一起來逐漸將地圖縮小。

為了讓你的程式支援縮放功能,需要在類別中實作觸控事件的處理,也就是UIScrollView的代理方法 viewForZoomingInScrollView() 的回傳。 該類別會負責追蹤畫面上手指數,以及按下數。當它偵測按一下,按兩下,或者兩隻手指觸控,它會做出相對的反應。假如是按兩下,以及兩隻手指觸控的事件,它可以用程式指定適當的因子來縮放滾動視圖。

針對我們的App,我們將會實作按兩下來放到最大比例,反之如果在最大放大比例下按兩下,則可以縮到最小,跟照片App類似。

加入以下的函式至類別中。

func setupGestureRecognizer() {
    let doubleTap = UITapGestureRecognizer(target: self, action: "handleDoubleTap:")
    doubleTap.numberOfTapsRequired = 2
    scrollView.addGestureRecognizer(doubleTap)
}
    
func handleDoubleTap(recognizer: UITapGestureRecognizer) {
        
    if (scrollView.zoomScale > scrollView.minimumZoomScale) {
        scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
    } else {
        scrollView.setZoomScale(scrollView.maximumZoomScale, animated: true)
    }
}

然後在 viewDidLoad() 底部做以下的呼叫。

setupGestureRecognizer()

在以上的程式,我們加入手勢控制器至scrollview,這辨識使用者是否有按兩下,然後我們依照目前的縮放程度來處理放大或縮小的需求。

執行這個App,你應該能夠按兩下來放大或縮小了。

在介面建構器中建立滾動視圖

在介面建構器實作滾動視圖比直接寫在程式中還來得簡單許多。 使用Storyboard只有幾個步驟就可以完成跟剛剛所建立的一樣內容。

在 Main.storyboard,拖曳另一個視圖控制器至畫面中。並設定它為初始視圖控制器(initial view controller),你可以將Storyboard Entry Point箭頭拖曳至該控制器,或者在新的視圖控制器的屬性檢閱器(Attributes Inspector)中勾選 Is Initial View Controller來設定。

A加入一個滾動視圖至視圖控制器的視圖,並定位其所有的邊緣,讓它能夠充滿整個畫面。

image07

然後加上一個圖像視圖(Image View)至Scroll View,並定位其所有邊緣對齊滾動視圖。

image08

記得一個滾動視圖需要知道其內容大小,當你設定圖像視圖的圖像,它的大小會被用來作為滾動視圖的內容大小。

在屬性檢閱器,選取image.png 作為圖像視圖的圖片。透過更新frame來解決所有Auto Layout的問題。只要幾個步驟且不需要撰寫任何一行程式,執行App後,你將得到跟之前一樣的滾動視圖。你可以看一下屬性檢閱器,選取Scroll View,看看裡面有什麼屬性,譬如你可以設定它的最小與最大縮放比例。

要縮放的話,你還是需要實作跟前面一樣的 viewForZoomingInScrollView() d代理方法。這裏我不打算介紹,因為跟前面一段會有所重複。這表示,倘若你需要更多滾動視圖的功能,你還是需要寫些程式。

巢狀滾動視圖

要將一個滾動視圖嵌入另一個滾動視圖是可行的。嵌入的方式可以是相同或者交叉方向來嵌入。這篇教學的所需要的程式是參考專案模板中的 NestedScrollViews 。

相同方向的滾動

相同方向的滾動是當一個UIScrollView的子視圖在UIScrollView間以相同方向來滾動。你可以在主滾動視圖的視圖中加入另一個區塊,來將部分資料做個別的呈現。你也可以使用相同方向滾動視圖來達到一些像是視差特效。在我們的範例App中,我們會使用相同方向的滾動,並針對兩個不同的滾動視圖設定不同的滾動速度。這樣會造成兩個視圖有視差的視覺效果。

打開NestedScrollViews專案的Storyboard,你應該會見到一個視圖控制器上的主視圖內有兩個滾動視圖。這些滾動視圖有Background 與 Foreground的ID,Background的滾動視圖上面有一個圖像視圖,其邊緣都與滾動視圖對齊,沒有留白。這個圖像已經設為 image.png,兩個滾動視圖與該視圖間都無間距。

在Foreground 滾動視圖,有一些標籤加在上面以及一個容器視圖(container view),這些標籤只是讓我們在執行App時有一個具備內容的滾動視圖。這個容器視圖將會在下一段中的交叉方向滾動中用到。倘若你想要較長尺寸的視圖控制器,你可以選取視圖控制器,然後至尺寸檢閱器中,變更Simulated Size 為 Freeform,就可以設定其尺寸。在這個範例,我將高度提高為1,200。這只是協助你在處理視圖時,如果需要更多空間在視圖控制器上呈現的作法。這並不會影響App的執行。這個對於在你正在佈局的元件,譬如滾動視圖下半部的元素,有能可在執行App時會超出視圖外的處理很方便。

image09

既然UI已經設定好了,視差特效的建立將會變快許多。首先執行App,並注意Foreground的滾動,Background還是保持不動。

我們會先建立兩個滾動視圖的outlet。打開助理編輯器(Assistant Editor),並建立一個outlet給Background 滾動視圖,並命名為background,對於Foreground的,則命名為foreground。你應該在ViewController.swift 中有以下的程式。

@IBOutlet weak var background: UIScrollView!
@IBOutlet weak var foreground: UIScrollView!

我們需要知道何時foreground視圖已經滾動,所以我們才可以計算背景背景視圖應該滾動的量,然後使用這個值來滾動它。這裏我們會使用UIScrollViewDelegate 方法。

變更ViewController 類別宣告如下。

class ViewController: UIViewController, UIScrollViewDelegate {

將以下這行加至 viewDidLoad()下方。我們只關心foreground,所以我們不會設定background的代理。

foreground.delegate = self

將以下函式加進類別中。

func scrollViewDidScroll(scrollView: UIScrollView) {
        
    let foregroundHeight = foreground.contentSize.height - CGRectGetHeight(foreground.bounds)
    let percentageScroll = foreground.contentOffset.y / foregroundHeight
    let backgroundHeight = background.contentSize.height - CGRectGetHeight(background.bounds)
        
    background.contentOffset = CGPoint(x: 0, y: backgroundHeight * percentageScroll)
}

在以上的程式中,我們取得了foreground的高度,並計算foreground 滾動多少量。取得這個值之後,我們將它乘上background的高度,使用它來設定background的 contentOffset 如此一來,在每一次foreground滾動時,可以讓background比foreground跑得快一點。執行這個App,你將會見到這個視差特效。

sv01

交叉方向滾動

交叉方向滾動是表示另一個滾動視圖的子視圖與滾動視圖呈現90度的交叉滾動,接下來我們要來建立它。

在NestedScrollViews 專案中,你會在Foreground滾動視圖中注意到一個容器視圖。這也是水平滾動視圖置放之處。

加上一個視圖控制器至介面建構器中。按住Control鍵,並從容器視圖中拖曳至新增的視圖控制器,並選取 embed segue。在選取完這個視圖控制器之後,至尺寸檢閱器,並變更其 Simulated Size 為 Freeform ,並設定它的高度為 128。128是我們容器視圖的尺寸,所以我們設定這個值為模擬視圖(simulated view)的尺寸,這樣有助於我們來看最後滾動視圖的樣貌。視圖控制器如下所示。

image10

拖曳一個滾動視圖至視圖控制器並定位邊緣如下所示。

image11

然後加入一個視圖至滾動視圖,並在尺寸檢閱器(Size Inspector)設定其尺寸為 70x70。將它放到滾動視圖的左側,然後複製這個視圖佈滿整個滾動視圖,視圖間的間距不需要太準確,如下圖所示。我變更了視圖的Background Color 為淡灰色,讓它有所區分。

image12

選取最左邊的視圖,並將其定位在上方以及左側,另外也加入Height與Width的約束條件。

image13

選取最右邊的視圖,並定位它的上方以及右側,並加入Width與Height的約束條件。

image14

在文件大綱中,選取Scroll View並做以下選取,Editor > Resolve Auto Layout Issues > All Views > Add Missing Constraints。這會加入約束條件至其他視圖,執行這個App,你應該能夠見到跟前面一樣的垂直滾動,但是在容器視圖,你也可以水平滾動內容,如下例所示,我設定視圖控制器的Background Color為Clear Color

scroll view demo

本篇的教學就到尾聲了,我們沒有介紹所有滾動視圖的內容,但是我希望這篇文章可以協助你設計滾動視圖。針對其他更多有關滾動視圖的主題,你可以進一步閱讀滾動視圖程式設計指南.

為了讓您方便參考,你可以在這裏下載這兩個完整專案檔.

譯者簡介:王豪勳 -渥合數位服務創辦人,畢業於台灣大學應用力學研究所,曾在半導體產業服務多年,近年來專注於協助客戶進行App軟體以及網站開發,平常致力於研究各式最軟硬體技術,擁有多本譯作。
原文: A Beginner’s Guide to UIScrollView
作者
Joyce Echessa
作為有經驗的網頁開發者同時從事手機程式開發的工作。閒時喜愛在網絡上發表教學文章,過程有樂趣也有挑戰,所謂教學相長,教與學的互動之中雙?
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。