#Task-12 Scroll View Delegate - 製作可縮放圖片的照片瀏覽分頁的三種方法- (1) Scroll View、(2) Collection View
研究 UIScrollViewDelegate protocol 的相關功能,用 Scroll View 搭配 Page Control 製作可縮放圖片的照片瀏覽分頁,本篇分別介紹以 Scroll View 跟 Collection View 兩種方法來製作
圖片瀏覽真的是很常見的功能,幾乎各種類型的 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 連結
參考資料
圖片來源