#24 Banner無限循環和自動輪播

Ethan
彼得潘的 Swift iOS / Flutter App 開發教室
11 min readMay 9, 2024

In UIKit

前言:
寫到後來app功能越加完善時,相對使用到的功能越會越來越多,認為需要做些筆記、寫些文章記錄一下要怎麼使用、為何要這樣用才行,程式語言跟現實的語言一樣,時間久了真的會忘。

在網頁或是app上面常看到自動播放的banner,這次寫一下兩種表現banner的方法:
1. 只有Scroll View。
2. Scroll View搭配Segmented Control。
< 這兩種操作方式,使用者也可以使用手勢去滾動Scroll View >

只有Scroll View:

先上結果

兩者可以看到不太一樣的地方,一個是有鼠標,一個則無;鼠標代表著我們的手去滾動Scroll View,而沒有鼠標的代表自動播放。

首先需要把想做banner的Scroll View做好,
可以參考以下:

今天的範例,我想把貓、雞、狗三張圖片輪播,輪播的概念就是圖片播放到狗之後會再跳回到第一張貓,第一張貓會跳回到最後一張狗。

第一步先設計顯示圖片的大小,配合15pro的手機大小,將imageView寬度設計成393,每張緊接著排著。
因為要輪播,設計的概念是三張圖片要在前後放兩張不會顯示到手機螢幕上的圖片:

如上圖,當圖片跑到座標(0,0)時,觸發程式說到(0,0)了,圖片要切換到(1179,0)的位置;反之當圖片跑到座標(1572,0)時,觸發程式說到(1572,0)了,圖片要切換到(393,0)的位置。

所以實際上:

先將Scroll View拉outlet:我將此變數聲明為bannerScrollView

這邊要注意此變數的類是UIScrollView,畢竟我們是ScrollView嘛~
UIScrollView本身提供基本的滾動和縮放功能,但是為了讓開發者能夠自定義和擴展這些行為,UIScrollView 支持一個叫做 delegate 的模式。

下面我請GPT幫忙整理了一個
一些 UIKit 中常見 UI 元件的 delegate 名稱以及它們的主要功能:

知道UIScrollView有一個叫做 UIScrollViewDelegate 的Protocol(協定)
我們就可以在ViewController加上這個UIScrollViewDelegate:

或是extension UIScrollViewDelegate

最重要一點請記得,我們是要委託ViewController做代理人,所以記得要拉線、記得要拉線、記得要拉線這個很重要!所以說三次!
沒有拉線最後寫完,還是無法如期顯示成果。
(因為你沒指定代理人嘛,所以根本不會有人幫你)

如果不拉線的話也可以,那就要到viewDidLoad()加上 bannerScrollView.delegate = self

詳細原因可以參考這一篇

完成上述後,回到ViewController,我們可以思考我們需要什麼:

  1. 在初始頁面時,呼叫bannerScrollView第一頁。
    showFirstBannerView()
  2. UIScrollViewDelegate的func 做輪播的操作。
    scrollViewDidEndDecelerating()
  3. 既然要自動輪播,就需要有一個計時的,告訴什麼時候要切換到下一張圖片。
    startTimer()
  4. 既然有計時,那就要有停止計時。
    stopTimer()

按照上面所求,先把需要的功能還有變數加到程式碼上:

為什麼需要stopTimer() ?

節省資源:當用戶離開這個視圖時,計時器若繼續運行,將不必要地消耗 CPU 和電池。

避免淺在錯誤:如果計時器引用了 ViewController 或其視圖的一些元素,而這些元素在視圖控制器被釋放後仍被計時器的回調使用,可能會導致應用崩潰。(白話一點講就是避免app crash)

保持應用邏輯的清晰:通過在視圖將要消失時停止計時器,可以確保相關的任務只在視圖可見時執行,這有助於維持應用邏輯的清晰和正確。

詳細使用後面再說明。

showFirstBannerView()

這邊要補充說明
bannerScrollView.frame.width
bannerScrollView.bounds.width
為什麼是用bounds 而不是frame

frame.width 是獲取視圖在其父視圖座標系中的寬度,這個值會因為縮放、旋轉變形而改變。
bound.width是獲取視圖在自己的座標系中的寬度,是原始、未經變換的寬度。

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)

為什麼我知道是這個?其實可以透過command+ 點擊UIScrollViewDelegate 知道這個protocol有什麼func:

怎麼設定圖片切換就不再解釋
(可以看註解說明或是下載我的範例研究研究:D)

start, stop的func

start, stop的func執行
viewWillAppear, viewWillDisappear

override func 我們可以透過一些關鍵字呼救出來:

ViewController常見的生命週期方法

我們可以在這邊看到有很多常見的生命週期方法

接著請GPT整理之:
(有機會再來整理一篇使用時機)

會使用viewWillAppear不是viewDidLoad原因:

我認為每次視圖即將進入這個畫面時,計時器都會被重新啟動,使用者來回切換視圖時這樣設計程式更符合邏輯。此外,這樣可以避免在視圖首次加載時和每次視圖將顯示時重複啟動計時器,避免淺在錯誤導致crash。
(另外apple有出viewIsAppearing iOS13以上的版本可以用,有興趣可以自己替換看看)

viewWillAppear及viewWillDisappear實際應用:
我們在這兩個func加上print

我們為這個範例加一個按鈕:
可以從下圖看出來按下按鈕會導航到另一個頁面

當我們開始執行app時
startTimer()就會開始執行,
也就是會列印「開始計時器」;

按下按鈕之後,就會導航到新的頁面,
此時會執行stopTimer(),
也就是會列印「停止計時器」;
(如果沒有stopTimer(),此時此刻banner還在繼續自動輪播)

如果按back返回,startTimer()就會再次開始執行,
也就是會列印「開始計時器」。

到這裡也許有人也會問delegate的事情:
如果不是在 storyboard 從UIScrollView 連到 view controller
我們不是會在viewDidLoad()加上 bannerScrollView.delegate = self嗎~
那為什麼不是加在viewWillAppear()?

這是因為是viewDidLoad()在視圖控制器的生命週期中只調用一次的方法,當視圖加載到記憶體中時會觸發;
viewWillAppear()每次視圖即將出現在螢幕上時都會被調用,包括了視圖首次出現時以及從其他視圖返回時。如果將代理設置放在這裡,每次視圖顯示之前都會重複設置代理。

結論:
放在viewDidLoad()的原因
1. 只需設置一次的配置
2.避免重複設置
3.效能考量

Here’s my GitHub repository link: (Scroll View)

Scroll View搭配Page Control:

先上結果

一樣一個有鼠標,一個是自動播放,下面的pageControl會隨著圖片切換時一起切換。

我們就延續上個專案,再加上pageControl就好,這樣也省了重新解釋的時間~

我們要page control隨著圖片變化而改變(outlet)、點選page control做圖片切換(action),因此需要把page control拉兩條線:
1. IBOutlet 我取名為pageControl ,記得type是UIPageControl
2. IBAction我取名為changePageControl,記得type是UIPageControl

幫page control加上切圖片的功能,已經請gpt加上註解

初始頁面的showFirstBannerView()記得要讓page control = 0

scrollViewDidEndDecelerating無限循環的func補上page control需要做的事~

最後startTimer()

Here’s my GitHub repository link: (Scroll View with page control)

沒想到寫一則文章要花兩天寫….🫠🫠

--

--