--

Scroll View Delegate 製作可縮放圖片的照片瀏覽分頁方法二- Collection View(月亮主題)

使用 Collection View 製作可縮放圖片的照片瀏覽分頁

Storyboard design

畫面主要 UI 元件
1. Collection View -> 內含用來縮放圖片的 Scroll View -> Scroll View 內含放照片的 Image View
2. Page Control

Step 1:
*在畫面新增一個 Collection View,會自動產生 Collection View Cell 跟 Content View
*command + Collection View & Safe Area 後上下左右對齊
*新增一個 Page Control 到自己想要的位置,並依照需求設定屬性 # of Pages 的數量

Step 2:
*新增一個 Scroll View 到 Content View 底下,用來縮放圖片
*設定 Scroll View 跟 Content View 上下左右對齊
*新增一個 Image View 到 Scroll View 底下
*讓 Image View 跟 Scroll View 的 Content Layout Guide 上下左右對齊

Swift Code

Step 1:
*新增 2 個 Cocoa Touch Class,一個 UIViewController,連結主要畫面,一個 UICollectionViewCell,用來連結剛剛新增的 Collection View 底下的 Collection View Cell
*設定 Collection View Cell 的 Reuse Identifier(Attributes inspector 分頁設定 Identifier)

Step 2:
* Collection View Cell 做 cell 內容的相關設定
>>宣告變數(Scroll View、Image View): 從 Main Storyboard 點選 >>Collection View Cell 按 control 拉線到 Scroll View 跟 Image View,點選剛剛宣告的變數連結 outlet

class CollectionMethodViewCell: UICollectionViewCell {

@IBOutlet weak var imageScrollView: UIScrollView!
@IBOutlet weak var moonImageView: UIImageView!

>>設定圖片在 Scroll View 內維持置中
>>設定圖片的縮放範圍

//讓scroll view 內的圖片縮放時維持置中
func updateScrollerViewContentInset(){
//讀取圖片的寬跟高
if let imageWidth = moonImageView.image?.size.width,
let imageHeight = moonImageView.image?.size.height{
//計算圖片等比例縮小到能放進顯示框中的時候,圖片寬跟高與顯示框的寬跟高的留白差距,因為置中時圖片左右兩邊或上下兩邊跟顯示框的距離會是一樣的,所以除以2
let insetWidth = (bounds.width - imageWidth * imageScrollView.zoomScale) / 2
let insetHeight = (bounds.height - imageHeight * imageScrollView.zoomScale) / 2
// scroll view 內容的上下左右可加入稱為 contentInset 的空白區塊
imageScrollView.contentInset = .init(top: max(insetHeight, 0), left: max(insetWidth, 0), bottom: 0, right: 0)
}
}
//scroll view 內的圖片縮放
func updateScrollerViewZoom(){
if let imageSize = moonImageView.image?.size{

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

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

//scrollViewDidZoom有可能還沒處發,所以這邊先呼叫一次
updateScrollerViewContentInset()
}
}

>>撰寫 UIScrollViewDelegate 的 extension

extension CollectionMethodViewCell: UIScrollViewDelegate{
func viewForZooming(in scrollView: UIScrollView) -> UIView? {// return a view that will be scaled. if delegate returns nil, nothing happens
return moonImageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView){ // any zoom scale changes
updateScrollerViewContentInset()
}

>>設定 Collection View Cell 為 Scroll View 的 Delegate
在 Main Storyboard 點選 Scroll View 按 control 拉線到 Collection View Cell,連結 Delegate 選項

CollectionMethodViewCell: UICollectionViewCell


// CollectionMethodViewCell.swift
import UIKit
class CollectionMethodViewCell: UICollectionViewCell {

@IBOutlet weak var imageScrollView: UIScrollView!
@IBOutlet weak var moonImageView: UIImageView!

//讓scroll view 內的圖片縮放時維持置中
func updateScrollerViewContentInset(){
//讀取圖片的寬跟高
if let imageWidth = moonImageView.image?.size.width,
let imageHeight = moonImageView.image?.size.height{
//計算圖片等比例縮小到能放進顯示框中的時候,圖片寬跟高與顯示框的寬跟高的留白差距,因為置中時圖片左右兩邊或上下兩邊跟顯示框的距離會是一樣的,所以除以2
let insetWidth = (bounds.width - imageWidth * imageScrollView.zoomScale) / 2
let insetHeight = (bounds.height - imageHeight * imageScrollView.zoomScale) / 2
// scroll view 內容的上下左右可加入稱為 contentInset 的空白區塊
imageScrollView.contentInset = .init(top: max(insetHeight, 0), left: max(insetWidth, 0), bottom: 0, right: 0)
}
}

//scroll view 內的圖片縮放
func updateScrollerViewZoom(){
if let imageSize = moonImageView.image?.size{

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

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

//scrollViewDidZoom有可能還沒處發,所以這邊先呼叫一次
updateScrollerViewContentInset()
}
}
}
extension CollectionMethodViewCell: UIScrollViewDelegate{
func viewForZooming(in scrollView: UIScrollView) -> UIView? {// return a view that will be scaled. if delegate returns nil, nothing happens
return moonImageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView){ // any zoom scale changes
updateScrollerViewContentInset()
}
}

Step 3:
* UIViewController 程式
>>先拉 outlet 宣告變數(Collection View、Page Control)
>>讓 UIViewController 遵從 UICollectionViewDelegate、UICollectionViewDataSource protocol,這時候 Xcode 會問你要不要加入 UICollectionViewDataSource 的 protocol stubs,點 Fix 就會自動幫你加入應該加入的 function
>>第一個 collectionView(_:numberOfItemsInSection:) 只要回傳有幾張照片就好
>>第二個 collectionView(_:cellForItemAt:) 要告訴他你的 cell 的索引路徑跟內容設置

class CollectionMethodViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
@IBOutlet weak var imageCollectionView: UICollectionView!
@IBOutlet weak var imagePageControl: UIPageControl!

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//圖片數量
return 3
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(CollectionMethodViewCell.self)", for: indexPath) as? CollectionMethodViewCell else{
fatalError("dequeueReusableCell CollectionMethodViewCell failed")
}

cell.moonImageView.image = UIImage(named: "moon\(indexPath.item + 1)")
return cell
}

>>設定 Flow Layout,Flow Layout 會決定 Collection View 裡的物件大小跟呈現方式

//itemSize:項目的預設大小,顯示時以此大小呈現
//estimatedItemSize:項目的估計大小,提供此值有助於提升 collection view 的效能,當項目還未顯示在畫面上時,會被假定為此大小
//minimumInteritemSpacing:同一行項目間的最小距離
//minimumLineSpacing:行跟行之間的最小距離
//sectionInset:section 的邊距,可以設置上下左右的邊距,預設皆為 0
func setupFlowLayout(){
let flowLayout = imageCollectionView.collectionViewLayout as? UICollectionViewFlowLayout
flowLayout?.itemSize = imageCollectionView.bounds.size //項目的預設大小為collection view的大小
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = 0 //同一行項目間的最小距離
flowLayout?.minimumLineSpacing = 0 //行跟行之間的最小距離
flowLayout?.sectionInset = .zero //section的邊距
}

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

//UICollectionViewDelegate optional func: 會在物件被加進去之前先呼叫此 function,我們可以在這裡對物件做額外的設置
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath){
guard let collectionViewCell = cell as? CollectionMethodViewCell else{
fatalError("willDisplay UICollectionViewCell failed")
}
collectionViewCell.updateScrollerViewZoom()
//呼叫updateScrollerViewZoom function設定cell要顯示的初始狀態
//updateScrollerViewZoom的function寫在CollectionMethodViewCell的Class中
}

>> UIViewController 為 UICollectionView 的 dataSource & delegate

override func viewDidLoad() {
super.viewDidLoad()
imageCollectionView.dataSource = self
imageCollectionView.delegate = self
setupFlowLayout()
// Do any additional setup after loading the view.
}

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


@IBAction func chagePage(_ sender: UIPageControl) {

//畫面捲動是靠collection view執行,所以用collection View的寬乘以當前頁數,來得到collection view應該捲動到的x座標
let newPoint = CGPoint(x: imageCollectionView.bounds.width * CGFloat(sender.currentPage), y: 0)
//設定collection View應該捲動到的新座標為剛剛算出來newPoint
imageCollectionView.setContentOffset(newPoint, animated: true)
}

CollectionMethodViewController: UIViewController

//
// CollectionMethodViewController.swift
//

import UIKit

class CollectionMethodViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
@IBOutlet weak var imageCollectionView: UICollectionView!
@IBOutlet weak var imagePageControl: UIPageControl!

func setupFlowLayout(){
let flowLayout = imageCollectionView.collectionViewLayout as? UICollectionViewFlowLayout
flowLayout?.itemSize = imageCollectionView.bounds.size //項目的預設大小為collection view的大小
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = 0 //同一行項目間的最小距離
flowLayout?.minimumLineSpacing = 0 //行跟行之間的最小距離
flowLayout?.sectionInset = .zero //section的邊距
}

override func viewDidLoad() {
super.viewDidLoad()
imageCollectionView.dataSource = self
imageCollectionView.delegate = self
setupFlowLayout()
// Do any additional setup after loading the view.
}


func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//圖片數量
return 3
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(CollectionMethodViewCell.self)", for: indexPath) as? CollectionMethodViewCell else{
fatalError("dequeueReusableCell CollectionMethodViewCell failed")
}

cell.moonImageView.image = UIImage(named: "moon\(indexPath.item + 1)")
return cell
}

//UICollectionViewDelegate optional func: 會在物件被加進去之前先呼叫此 function,我們可以在這裡對物件做額外的設置
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath){
guard let collectionViewCell = cell as? CollectionMethodViewCell else{
fatalError("willDisplay UICollectionViewCell failed")
}
collectionViewCell.updateScrollerViewZoom()
//呼叫updateScrollerViewZoom function設定cell要顯示的初始狀態
//updateScrollerViewZoom的function寫在CollectionMethodViewCell的Class中
}

@IBAction func chagePage(_ sender: UIPageControl) {

//畫面捲動是靠collection view執行,所以用collection View的寬乘以當前頁數,來得到collection view應該捲動到的x座標
let newPoint = CGPoint(x: imageCollectionView.bounds.width * CGFloat(sender.currentPage), y: 0)
//設定collection View應該捲動到的新座標為剛剛算出來newPoint
imageCollectionView.setContentOffset(newPoint, animated: true)
}
}
extension CollectionMethodViewController: UIScrollViewDelegate{
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let page = scrollView.contentOffset.x / scrollView.bounds.width
imagePageControl.currentPage = Int(page)
}
}

Demo

Demo

參考教程

--

--