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

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

Photo by Jaelynn Castillo on Unsplash

練習用 UIScrollViewDelegate protocol 的功能製作可以縮放圖片的照片瀏覽分頁,搭配 page control 顯示頁數,如下圖

先前一篇分別介紹了用 Scroll View 及 Collection View 的製作方法,連結在這兒

這篇介紹第三種,用 UIPageViewController 來做出同樣效果!

⭐️ 畫面佈局

我們需要的主要元件:

  • Root View Controller + Container View + Page Control 主畫面
  • Page View Controller 執行翻頁動作,被嵌入在 Container View 中
  • 裝子畫面的 View Controller,放置每頁顯示的內容,有幾頁就放幾個 View Controller

1️⃣ 在 Root View Controller 加入一個 Container View 跟 Page Control,讓 Container View 跟 Safe Area 上下左右對齊,作為內容顯示的大小範圍

2️⃣ 刪除原本 Container View 連結的 View Controller,並新增一個 Page View Controller

3️⃣ 從 Container View 拉 Segue 到 Page View Controller,選擇 Embed,會看到 Page View Controller 變成 Container View 的大小

4️⃣ 新增子畫面的 View Controller,有幾頁就新增幾個

每個子畫面的 View Controller 中會放入用來縮放圖片的 Scroll View,Scroll View 裡面裝放圖片的 Image View

子畫面待會會被裝到 Page View Controller 裡,再透過 Container View 顯示在主畫面上,所以子畫面的佈局會影響顯示結果,Auto Layout 就根據需求設定。這邊我同樣讓 Scroll View 跟 Safe Area 上下左右對齊,Image View 跟 Scroll View 的 Content Layout Guide 上下左右對齊、跟Frame Layout Guide 等寬等高

最後的 StoryBoard 畫面如下:

⭐️ 程式部分

💡 前置作業

  • 為每個子畫面 View Controller 建立 UIViewController 類別
  • 在每個子畫面 View Controller 的 Identity Inspector 設定 Class 類別跟 Storyboard ID
  • 為 Page View Controller 建立 UIPageViewController 類別,並同樣到 Identity Inspector 設定 Class 類別

💡 pageViewController 的程式
(設定翻頁邏輯、傳遞子畫面資訊給 Root View Controller 的元件)

1️⃣ 宣告儲存子畫面 View Controller 的 Array

class pageViewController: UIPageViewController {

var viewcontrollerList = [UIViewController]()

2️⃣ 宣告透過 Storyboard ID 取得子畫面 View Controller 的 function

func getViewController(withStoryboardID storyboardID: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: storyboardID)
}

3️⃣ 在 viewDidLoad 中用 Storyboard ID 生成子畫面 View Controller,加到剛剛宣告的 Array 中,並設定首頁

override func viewDidLoad() {
super.viewDidLoad()

//生成子畫面加到Array中
viewcontrollerList.append(getViewController(withStoryboardID: "firstVC"))
viewcontrollerList.append(getViewController(withStoryboardID: "secVC"))
viewcontrollerList.append(getViewController(withStoryboardID: "thirdVC"))
viewcontrollerList.append(getViewController(withStoryboardID: "fourthVC"))
viewcontrollerList.append(getViewController(withStoryboardID: "fifthVC"))
viewcontrollerList.append(getViewController(withStoryboardID: "sixthVC"))
//設定首頁
setViewControllers([viewcontrollerList[0]], direction: .forward, animated: true, completion: nil)

4️⃣ 遵從 UIPageViewControllerDataSource 來設定翻頁功能,這時候會跳出警告問你要不要加入 protocol stubs,點選 Fix 自動產生需要的 function

extension pageViewController: UIPageViewControllerDataSource{}

第一個 function 設定往前翻頁的邏輯,第二個 function 設定往後翻頁的邏輯

extension pageViewController: UIPageViewControllerDataSource{

//往前翻頁
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {

//取得當前的頁數
let currentPageIndex = viewcontrollerList.firstIndex(of: viewController)!
//如果頁等於0,表示當前已經是第一頁,往前翻不產生動作
if currentPageIndex == 0{
return nil
//如果頁數不等於0,往前翻顯示前一個子畫面
}else{
return viewcontrollerList[currentPageIndex - 1]
}
}

//往後翻頁
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {

//取得當前的頁數
let currentPageIndex = viewcontrollerList.firstIndex(of: viewController)!
//如果頁數等於Array中view controller數量-1,表示當前已經是最後一頁,往後翻不產生動作
if currentPageIndex == viewcontrollerList.count - 1{
return nil
//如果還不是最後一頁,往後翻顯示下一個子畫面
}else{
return viewcontrollerList[currentPageIndex + 1]
}
}
}

5️⃣ 在 viewDidLoad 中設定 pageViewController 本身代理 dataSource

self.dataSource = self

✨ 完成到這裡時,可以先執行看看,這時候已經可以達到翻頁效果囉!
不過 Page Control 還沒有作用,我們需要從 pageViewController 傳遞一些資訊給他才行

6️⃣ 宣告一個自定義的 protocol,用來傳遞頁數總共幾頁、當前頁數的資訊給在 Root View Controller 的 Page Control,並在 viewDidLoad 中讀取總頁數

//protocal寫在Class pageViewController: UIPageViewController {} 外面protocol pageViewControllerDelegate: AnyObject{

func numberOfPage(numberOfPage: Int) //總頁數
func pageIndex(index: Int) //當前頁數
}
//viewDidLoad
//設定總頁數為Array裡子畫面的數量
pageViewControllerDelegate?.numberOfPage(numberOfPage: viewcontrollerList.count)

7️⃣ 宣告 PageViewControllerDelegate

class pageViewController: UIPageViewController {

var viewcontrollerList = [UIViewController]()
weak var pageViewControllerDelegate: pageViewControllerDelegate?

8️⃣ 遵從 UIPageViewControllerDelegate,並新增 pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) function,設定翻頁完執行的動作

extension pageViewController: UIPageViewControllerDelegate{

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {

//讀取當前的子畫面
let currentPageViewController = (viewControllers?.first)!
//讀取當前的頁數
let currentIndex = viewcontrollerList.firstIndex(of: currentPageViewController)!
//儲存當前頁數到用來傳遞資料的pageIndex中
pageViewControllerDelegate?.pageIndex(index: currentIndex)

}
}

9️⃣ 在 viewDidLoad 中設定 pageViewController 本身代理 Delegate

    self.dataSource = self
self.delegate = self

pageViewController 的程式到這邊暫時告一段落!接下來要到主畫面做資料傳遞的相關設定

💡 Root View Controller 的程式
(讓 Page Control 的小圓點能跟著子畫面更新)

1️⃣ 宣告變數(Page Control、pageViewController)

class ViewController: UIViewController {@IBOutlet weak var pageControl: UIPageControl!var pageViewController: pageViewController!

2️⃣ 透過 prepare(for:sender:) 代理 pageViewControllerDelegate

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

//獲取pageViewController回傳的實例
if let destinationPageViewController = segue.destination as? pageViewController{
pageViewController = destinationPageViewController

//代理自定義的pageViewControllerDelegate
pageViewController.pageViewControllerDelegate = self

}
}

3️⃣ 遵從先前自定義的 PageViewControllerDelegate 的 protocol 來設定 Page Control 的總頁數及當前頁數

extension ViewController: pageViewControllerDelegate{    //設定總頁數
func numberOfPage(numberOfPage: Int) {
pageControl.numberOfPages = numberOfPage
}

//設定當前頁數
func pageIndex(index: Int) {
pageControl.currentPage = index
}
}

✨ 這時候執行看看,不只可以翻頁,翻頁後 Page Control 也會跟著更新囉!
只是如果是點選或拖曳 Page Control 的小圓點,App 會閃退,因為我們還沒有寫 Page Control 的 IBAction function

💡 點選/拖曳 Page Control,畫面跟著更新的功能

1️⃣ 先回到 pageViewController,新增一個自定義的 function,設定點選 Page Control 時子畫面的翻頁的方向

func goToPage(index: Int) {
//讀取當前頁面
let currentViewController = viewControllers!.first!
//讀取當前頁數
let currentViewControllerIndex = viewcontrollerList.firstIndex(of: currentViewController)!
//如果點選Page Control所得到的index值大於當前頁數的值,代表應該繼續往後翻
if index > currentViewControllerIndex{
setViewControllers([viewcontrollerList[index]], direction: .forward, animated: true, completion: nil)
//如果點選Page Control所得到的index值小於當前頁數的值,代表應該往前翻
}else if index < currentViewControllerIndex{
setViewControllers([viewcontrollerList[index]], direction: .reverse, animated: true, completion: nil)
}
}

2️⃣ 再到 Root View Controller,幫 Page Control 拉 Segue 撰寫 IBAction function

@IBAction func changePage(_ sender: UIPageControl) {
//讀取點選Page Control後,sender回傳的頁數
let currentPageIndex = sender.currentPage

//將讀取到的頁數套到剛剛定義的function中執行
pageViewController.goToPage(index: currentPageIndex)

}

✨ 上面完成後我們再執行看看,翻頁功能跟 Page Control 應有的功能有正常運作啦!

最後就剩下讓子畫面圖片能夠縮放的功能,要使用 ScrollViewDelegate,我們以第一個子畫面當作例子,其他子畫面用同樣方式設定即可哦!

💡 圖片縮放功能(ScrollViewDelegate)

1️⃣ 在 firstVC 類別中宣告變數(Scroll View、Image View)

class firstVC: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageView: UIImageView!

2️⃣ 從 Main Storyboard 的 firstVC 分別拉 Segue 到 Scroll View 跟 Image View 連結對應的 outlet,再點選 Scroll View 拉 Segue 到 firstVC,設定 firstVC 為 Scroll View 的 Delegate

--

--