#Task-12 Scroll View Delegate - 製作可縮放圖片的照片瀏覽分頁的三種方法- (1) Scroll View、(2) Collection View

研究 UIScrollViewDelegate protocol 的相關功能,用 Scroll View 搭配 Page Control 製作可縮放圖片的照片瀏覽分頁,本篇分別介紹以 Scroll View 跟 Collection View 兩種方法來製作

Photo by Jaelynn Castillo on Unsplash

圖片瀏覽真的是很常見的功能,幾乎各種類型的 App 都會出現,然後常常會搭配 page control 的小圓點顯示現在是第幾頁,例如 IG 的發文照片瀏覽、第一次使用 App 的功能導覽頁面等,都是類似的概念

這次先簡單的用幾張我哥最近跟我聊到的帥車照片(?),來練習用 UIScrollViewDelegate protocol 的功能製作可以縮放圖片的照片瀏覽分頁,搭配 page control 顯示頁數,如下圖

原本的作業應該是用 Scroll View 來做,不過彼得潘在文章中有提到通常照片瀏覽頁面會用 Collection View 或 UIPageViewController 實現,效率會比 scroll view 好,所以我決定三種方法都練習😂

篇幅的關係這篇先介紹 Scroll View 跟 Collection View 兩種!

第三種 UIPageViewController 的作法請看連結

⭐️ 第一種:使用 Scroll View 製作

💡 畫面佈局

畫面 UI 元件主要會分成三層,第一層放水平捲動的 Scroll View 跟 Page Control,第二層是 Stack View,裝著第三層放用來縮放圖片的 Scroll View 們(我取名叫 Content Scroll View,後面都以它代稱)

1️⃣ 新增一個用來水平捲動的 Scroll View 到畫面上,讓他跟 Safe Area 上下左右對齊,並在屬性欄位勾選 Paging Enabled,這樣當捲動到一定距離時他就會聰明的滑到上/下一張照片
(依自己需求設定 Auto Layout 即可,希望圖片佔滿整個螢幕就跟 View 對齊)

這時候會出現紅色警告,因為 Scroll View 還不知道自己的捲動範圍,等後面圖片內容設定好自然就解決囉!先繼續下去~

2️⃣ 新增一個用來縮放圖片的 Content Scroll View 到 Scroll View 裡,同時也新增一個 Image View 到 Content Scroll View 中,讓 Image View 跟 Content Scroll View 的 Content Layout Guide 上下左右對齊、跟 Frame Layout Guide 等寬等高

為了方便查看圖片的變化,這時候可以先設定 Image View 的圖片為要展示的圖片,不過晚點再設定也是可以的!

如果還不太了解 Content Layout Guide 跟 Frame Layout Guide 的功用跟差別,可以先參考彼得潘的文章:

懶人包:Content Layout Guide 決定 Scroll View 的捲動範圍;Frame Layout Guide 則是 Scroll View 自身在畫面上的框框大小範圍

以這邊的 Content Scroll View 來說,我們要用 Image View 的大小來決定他的捲動範圍,所以讓 Image View 跟 Content Scroll View 的 Content Layout Guide 上下左右對齊;而 Image View 的初始大小我們希望是 Content Scroll View 的大小,所以讓 Image View 跟 Frame Layout Guide 等寬等高

3️⃣ 把 Content Scroll View 選起來,按右下角的 Embed in ➜ Stack View,並將 Stack View 屬性的 Axis 設為 Horizontal,Alignment 設為 Fill,Distribution 設為 Fill Equally,Spacing 設為 0

4️⃣ 設定 Stack View 跟用來水平捲動的 Scroll View 的 Content Layout Guide 上下左右對齊,捲動範圍將由 Stack View 裡放的照片們的大小決定;設定 Content Scroll View 跟 Scroll View 的 Frame Layout Guide 等寬等高,讓 Content Scroll View 的大小等於 Scroll View 的大小

完成後如下圖,紅色警告已經消失啦!而 Image View 的大小也變成我們要的大小了!

目前為止的畫面結構跟 Constraints 設定如下:

5️⃣ 複製 Content Scroll View,貼在 Stack View 裡面,像我有 6 張圖就複製 5個出來,然後設定好要顯示的照片;最後新增一個 Page Control 元件到想要的位置,並依照照片數量設定屬性 # of Pages 的數量

6️⃣ 幫每個 Content Scroll View 設定 Zoom 的 Min 跟 Max 值,也就是可以放大跟縮小到什麼程度,我這邊設定 Min = 1,Max = 6,要設定這個待會寫完程式碼才能縮放圖片哦!

這樣畫面基本上就佈置跟設定完成!👍

完成到這邊去執行就已經可以水平捲動照片了!(如下圖)不過還不能縮放圖片,Page Control 的小圓點也還不會隨著更新,這些功能就要靠程式碼啦!

💡 程式部分

🔖 前製作業

  • 拉 outlet 宣告變數(Scroll View、Page Control、Image View)
import UIKitclass ViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var pageControl: UIPageControl!
@IBOutlet var contentImageView: [UIImageView]! //顯示的照片統一儲存在Array中
  • 設定 View Controller 為 Scroll View 跟 Content Scroll View 的 Delegate
    每個 Content Scroll View 都要設定哦!⭐️⭐️⭐️

因為圖片縮放需要讀取 Page Control 的 current page 的數值來判斷是哪張圖要被縮放,所以我們先來處理 Page Control 吧!

🔖 讓 Page Control 的小圓點隨著捲動頁數更新

撰寫 UIScrollViewDelegate 的 extension,利用 scrollViewDidEndDecelerating(_:) function 告訴 Delegate 捲動結束後執行的動作

extension ViewController: UIScrollViewDelegate{
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

//contentOffset.x是ScrollView捲動的值,除以ScrollView的寬就可以得到現在應該是捲動到第幾頁
let page = scrollView.contentOffset.x / scrollView.bounds.width

//設定currentPage更新為捲動到的頁數
pageControl.currentPage = Int(page)
}
}

🔖 點選或拖曳 Page Control,顯示的照片跟著捲動更新

拉 Page Control 的 IBAction function,sender 的對象設為 UIPageControl

@IBAction func changePage(_ sender: UIPageControl) {

//用Scroll View的寬乘以當前頁數,得到Scroll View應該捲動到的x座標
let newPoint = CGPoint(x: scrollView.bounds.width * CGFloat(sender.currentPage), y: 0)

//設定Scroll View應該捲動到的新座標為剛剛算出來newPoint
scrollView.setContentOffset(newPoint, animated: true)

}

🔖 縮放圖片

同樣寫在 UIScrollViewDelegate 的 extension,利用 viewForZooming(in:) function 請求 Delegate 縮放圖片

extension ViewController: UIScrollViewDelegate{
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
//contentOffset.x是ScrollView捲動的值,除以ScrollView的寬就可以得到現在應該是捲動到第幾頁
let page = scrollView.contentOffset.x / scrollView.bounds.width
pageControl.currentPage = Int(page)
}

func viewForZooming(in scrollView: UIScrollView) -> UIView? {

//以Page Control的所在頁數判斷現在是哪一張圖片要進行縮放動作
return contentImageView[pageControl.currentPage]
}

}

第一種方法完成!🙌

— — — — — —

⭐️ 第二種:使用 Collection View 製作

💡 畫面佈局

畫面 UI 元件主要會分成三層,第一層放Collection View 跟 Page Control,第二層是用來縮放照片的 Scroll View,裝著第三層是放照片的 Image View

1️⃣ 在畫面新增一個 Collection View,會自動產生 Collection View Cell 跟 Content View,讓 Collection View 跟 Safe Area 上下左右對齊;接著新增一個 Page Control 到自己想要的位置,並依照需求設定屬性 # of Pages 的數量

2️⃣ 新增一個 Scroll View 到 Content View 底下,用來縮放圖片,並設定 Scroll View 跟 Content View 上下左右對齊;最後再新增一個 Image View 到 Scroll View 底下,讓 Image View 跟 Scroll View 的 Content Layout Guide 上下左右對齊

畫面佈局就這樣,相較之下似乎有簡潔一點? 😂

💡 程式部分

1️⃣ 建立新的 Class 類別

新增 2 個 Cocoa Touch Class,一個 UIViewController(我取叫 collectionViewController),連結主要畫面,一個 UICollectionViewCell(我取叫 collectionViewCell),用來連結剛剛新增的 Collection View 底下的 Collection View Cell

2️⃣ 設定 Reuse Identifier

點選 collectionViewCell,切換到 Attributes inspector 分頁設定 Identifier 的名稱

3️⃣ 先在 collectionViewCell 做 cell 內容的相關設定

  • 宣告變數(Scroll View、Image View)
import UIKitclass collectionViewCell: UICollectionViewCell {

@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageView: UIImageView!

從 Main Storyboard 點選 collectionViewCell 按 control 拉線到 Scroll View 跟 Image View,點選剛剛宣告的變數連結 outlet

  • 設定圖片在 Scroll View 內維持置中
func updateContentInset(){

//讀取圖片的寬跟高
if let imageWidth = imageView.image?.size.width, let imageHeight = imageView.image?.size.height{

//計算圖片等比例縮小到能放進顯示框中的時候,圖片寬跟高與顯示框的寬跟高的留白差距,因為置中時圖片左右兩邊或上下兩邊跟顯示框的距離會是一樣的,所以除以2
let insetWidth = (bounds.width - imageWidth * scrollView.zoomScale) / 2
let insetHeight = (bounds.height - imageHeight * scrollView.zoomScale) / 2
scrollView.contentInset = .init(top: max(insetHeight, 0), left: max(insetWidth, 0), bottom: 0, right: 0)
}
}

有沒有寫這段程式碼的差距如下兩圖:

沒寫的圖片會自動靠左,有寫的置中
  • 設定圖片的縮放範圍
func updateZoom(){
if let imageSize = imageView.image?.size{

//計算顯示框的寬跟高和圖片的寬跟高的比率
let widthScale = bounds.size.width / imageSize.width
let heightScale = bounds.size.height / imageSize.height

//兩者間較小的比率就是圖片能縮放到完整顯示在顯示框的比率
let scale = min(widthScale, heightScale)
scrollView.minimumZoomScale = scale //scroll view內容最小的縮放範圍
scrollView.maximumZoomScale = max(widthScale, heightScale) //scroll view內容的最大的縮放範圍
scrollView.zoomScale = scale //scroll view內容大小的初始值

//scrollViewDidZoom有可能還沒處發,所以這邊先呼叫一次
updateContentInset()
}
}
  • 撰寫 UIScrollViewDelegate 的 extension
extension collectionViewCell: UIScrollViewDelegate{

func viewForZooming(in scrollView: UIScrollView) -> UIView? {
imageView //縮放時影響的對象
}

func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateContentInset()
}
}
  • 設定 collectionViewCell 為 Scroll View 的 Delegate

在 Main Storyboard 點選 Scroll View 按 control 拉線到 collectionViewCell,連結 Delegate 選項

4️⃣ 寫 collectionViewController 的程式

  • 先拉 outlet 宣告變數(Collection View、Page Control)
import UIKitclass collectionViewController: UIViewController{

@IBOutlet weak var collectionView: UICollectionView!
@IBOutlet weak var pageControl: UIPageControl!
  • 讓 collectionViewController 遵從 UICollectionViewDelegate、UICollectionViewDataSource protocol,這時候 Xcode 會問你要不要加入 UICollectionViewDataSource 的 protocol stubs,點 Fix 就會自動幫你加入應該加入的 function
class collectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

第一個 collectionView(_:numberOfItemsInSection:) 只要回傳有幾張照片就好

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}

第二個 collectionView(_:cellForItemAt:) 要告訴他你的 cell 的索引路徑跟內容設置

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

//設定cell的索引路徑
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(collectionViewCell.self)", for: indexPath) as! collectionViewCell

//設置cell內容
cell.imageView.image = UIImage(named: "fabia-\(indexPath.item + 1)")

return cell
}
  • 設定 Flow Layout,Flow Layout 會決定 Collection View 裡的物件大小跟呈現方式
func setupFlowLayout(){
let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
flowLayout?.itemSize = collectionView.bounds.size //項目的預設大小為collection view的大小
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = 0 //同一行項目間的最小距離
flowLayout?.minimumLineSpacing = 0 //行跟行之間的最小距離
flowLayout?.sectionInset = .zero //section的邊距
}

➜ itemSize:項目的預設大小,顯示時以此大小呈現

➜ estimatedItemSize:項目的估計大小,提供此值有助於提升 collection view 的效能,當項目還未顯示在畫面上時,會被假定為此大小

➜ minimumInteritemSpacing:同一行項目間的最小距離

➜ minimumLineSpacing:行跟行之間的最小距離

➜ sectionInset:section 的邊距,可以設置上下左右的邊距,預設皆為 0

  • 新增 collectionView(_:willDisplay:forItemAt:) function,Collection View 會在物件被加進去之前先呼叫此 function,我們可以在這裡對物件做額外的設置
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

let collectionViewCell = cell as? collectionViewCell

collectionViewCell?.updateZoom()
//呼叫updateZoom function設定cell要顯示的初始狀態
//updateZoom的function寫在collectionViewCell的Class中
}

以上完成之後就可以讓圖片水平捲動+縮放啦!神奇的是 Page Control 的小圓點也會自動跟著更新呢!不過如果點選或拖曳小圓點會閃退,因為我們還沒有寫觸發 Page Control 的執行動作

5️⃣ 拉 Page Control 的 IBAction function,sender 的對象設為 UIPageControl

@IBAction func changePage(_ sender: UIPageControl) {

//畫面捲動是靠collection view執行,所以用collection View的寬乘以當前頁數,來得到collection view應該捲動到的x座標
let newPoint = CGPoint(x: collectionView.bounds.width * CGFloat(sender.currentPage), y: 0)

//設定collection View應該捲動到的新座標為剛剛算出來newPoint
collectionView.setContentOffset(newPoint, animated: true)
}

最後我有把原本 Collection View 的 constrains 清除,不然執行的時候會有誤差🤔 所以一開始設定跟 Safe Area 上下左右對齊只是方便設定 Collection View 的大小跟位置(?

第二種方法完成!👏

附上兩種方式的 Github 連結

參考資料

圖片來源

--

--