C38.Page control / Segmented Control / Button/ Swipe Gesture 切換內容

成果

  • 只要是男人就是該看紅豬!

學習發想

這次就以宮崎駿的動畫為練習方向好了,因為這幾個功能的運用很像在Netflix、Disney+Uniqlo等有圖片畫面的App很常看到 ◉‿◉

  • 感覺這些切換功能的運用幾乎涵蓋了許多功能性的App,像是拍賣、影音或是社交軟體等都會運用!
  • 先看了其他人的作品,其實可以發現這些功能,蠻直覺的都可以跟ImageView 或是 TextView 去做搭配運用。
  • 同時這幾個功能彼此間也會互相組合運用,藉此讓使用者在操作上更有彈性去做快速切換的行為。
這個頁面同時加入了四種切換頁面功能

練習目標

因為這次的練習是將這幾個切換頁面的功能整合起來,所以UIImageView、TextView都是使用Ispector Attribute去執行屬性修改,藉此減少程式碼。目的是讓我自己可以更清晰地專注在IBAction、IBOutlet的功能連動運用上面。

  • 紅框:Next Button & Prev Button (按鈕頁面切換)
  • 黃框:Page Control(小圓點頁面切換)
  • 籃框:Segmented Control (欄位切換)
  • 紫框:Swipe Gesture(滑動手勢切換頁面)
  • function運用:使所有切換功能統整連動
  • 補:Long Press Gesture Recognizer(長按亂數切換頁面)
佈局圖

IBOutlet、Array

  • movieContentTextView:文本顯示。
  • movieSegmentedControl:欄位顯示。
  • pageControl:小圓點顯示。
  • movieCoverImageView:電影海報畫面。
  • movies:設置電影項目的Array,0、1、2 分別為魔女宅急便、紅豬、神隱少女。
  • moviesDescription:設置電影文本項目的Array。
  • index變數的用途是追蹤目前顯示的電影海報電影文本在movies陣列中的索引。陣列包含了三個字串元素,每個元素都代表一部電影的名稱以及文本。
    @IBOutlet weak var movieContentTextView: UITextView!
@IBOutlet weak var movieSegmentedControl: UISegmentedControl!
@IBOutlet weak var pageControl: UIPageControl!
@IBOutlet weak var movieCoverImageView: UIImageView!

// 電影海報項目
let movies = ["魔女宅急便", "紅豬", "神隱少女"]

// 電影文本設置
let moviesDescription = ["琪琪是一個實習魔女。依家族慣例,13歲的琪琪該離開家鄉,到新的城市展開獨立生活,修行成正式的魔女。她用她媽媽的掃帚,帶著她的同伴黑貓吉吉去開展她的新生活。\n有一天,琪琪因為心情鬱悶、對自己的行為感到疑惑時失去了法力。最終琪琪在她的新朋友兼畫家烏露絲拉的幫助下,克服了這些障礙。在一次飛船的意外中,琪琪為了拯救蜻蜓,在千鈞一髮之際恢復了飛行的能力並成功救了他。經過此事,琪琪的義舉獲得眾人的讚許與敬重,讓她重拾信心,繼續在城市裡執行送貨服務。", "第一次世界大戰時期義大利空軍的王牌飛行員波魯克・羅梭中了魔法變成了一頭豬。如今他成了一名賞金獵人,專門在亞得里亞海空域阻擊空中劫匪。劫匪們為了打倒羅梭從美國請來了超級飛行員卡地士。\n卡地士趁羅梭的飛機引擎發生故障之時將其擊落。羅梭把愛機送往老朋友保可洛的工廠修理,在那裡遇到了保可洛的孫女菲奧。菲奧完美地修復了羅梭的座機,和羅梭一起重返藍天。\n而等待他們的卻是那批空中劫匪。為了爭奪菲奧,卡地士向羅梭提出決鬥。在決鬥前夜,羅梭向菲奧講述了自己在一戰中經歷。次日,在大批空中劫匪以及菲奧的注視下,決鬥開始了。最後因與卡地士在空戰中無法分出高下,而改以肉搏拳賽打敗了卡地士,也贏得了榮耀。", "年僅十歲的少女千尋與父母一同驅車前往新家,途中迷路車子來到一道拱門前,千尋與父母下車穿越拱門到達一處荒廢的日式小鎮。千尋心生害怕,但是千尋的父母卻在未經主人同意的情況下,大大方方的取用食物,轉眼間,他們變成了兩頭豬。\n原來千尋與父母來到神靈休憩的小城,所有的食物都是呈現給神靈的供品,千尋的父母為了自己的慾望,取用了不屬於他們的食物。\n千尋在驚嚇之餘遇見了似曾相識的少年哈克,哈克對千尋伸出援手,教導千尋在神靈世界的生存之道。千尋在哈克的指點之下,向開設澡堂湯婆婆毛遂自薦。\n唯利是圖的湯婆婆在拗不過千尋的請託之下,答應讓千尋在她開設的澡堂裡工作,並且將千尋的名字改為「小千」。" ]

// index追蹤陣列
var index = 0

Next Button & Prev Button

  • 我一開始是先練習Button的切換,基本上只要把index的陣列運算寫好,後續其他切換功能都很好去套用。
Button圖片、文本切換

下一頁

  • index 變數是用來追蹤目前顯示的電影在 movies 陣列中的位置。每次按下「下一頁」的按鈕時,index 的值會加 1,但如果 index 已經到達了 movies 陣列的最後一個元素,那麼 index 就會被設置為 0,這樣就可以實現循環輪播的效果。
  • % 運算符,它的作用是取餘數。 index 等於 2 ,movies.count 的值本身為 3,那麼 (index + 1) % movies.count 的值就是 0,這樣就可以從頭再次開始顯示電影了。
  • 我當初想了一下,後來用圖來理解就很好懂惹XD
    // 下一頁
@IBAction func goToNextPage(_ sender: Any) {

// Button 下一頁 (movies.count = 3)
index = (index + 1) % movies.count

// movie封面
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)

// movie文本內容
movieContentTextView.text = moviesDescription[index]

}
  • 印出:index, moviesName

上一頁

  • index 值減 1 的目的是顯示前一個電影。例如,如果 index 值為 2,表示目前正在顯示電影列表中的第三個電影。如果要顯示前一個電影,則需要將 index 值減 1,這樣就可以獲得列表中的第二個電影。
  • 當使用 % 運算符時,可以保證結果始終在 0movies.count-1 的範圍內,這樣就可以實現循環顯示電影的效果。
        // 上一頁
@IBAction func goToPreviousPage(_ sender: Any) {

// Button 上一頁 (movies.count = 3)
index = (index + movies.count - 1) % movies.count

// movie封面
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)

// movie文本內容
movieContentTextView.text = moviesDescription[index]

}

Page Control (小圓點切換)

  • 當使用者滑動到另一個頁面時,Page Control 會自動更新當前頁面索引,從而在 UI 上提供更好的可讀性和使用體驗。可以知道當前頁面數量以及所在位置。
  • 點選小圓點只會切換至下一個 / 上一個點,不會直接跳到指定的小圓點。
連動
  • sender 是對應的 UIPageControl,而 currentPage 屬性會傳回目前選取的頁面索引值。
  • 此行程式碼 index = sender.currentPage 的目的是將 index 變數的值設為 UIPageControl 目前選取的頁面索引值,以便更新顯示的電影封面和說明文本。
    @IBAction func pageControlDidChange(_ sender: UIPageControl) {
// pageControl 當前頁面
index = sender.currentPage

// movie封面
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)

// movie文本內容
movieContentTextView.text = moviesDescription[index]

}

Page Control與Button連動

  • 除了小圓點本身可以點擊以外。因為此次是要將許多的頁面切換功能放在同一個畫面上,而Page Control本身有小圓點可以看到當前位置。
  • 如果再ButtonPage Control同時存在,又沒有將其連動,這樣會導致使用Button切換頁面時,小圓點的點並不會跟著跳到當前位置。
未連動:小圓點的點不會因Button而更新
  • 爲了點選Button時,小圓點的點也可以跟著連動,將其pageControl 的 outlet,設置當前頁面為index。
  • 並且放置到下一頁、上一頁的IBAction,這樣點選Button小圓點也會跟著變化。
        // 這邊以下一頁為範例    
@IBAction func goToNextPage(_ sender: Any) {

// Button 下一頁 (movies.count = 3)
index = (index + 1) % movies.count

// movie封面
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)

// movie文本內容
movieContentTextView.text = moviesDescription[index]

// pageControl 的 outlet 連動
pageControl.currentPage = index

}

Segmented Control

Segmented Control與其他功能連動
  • 每個Segment代表一部電影,因此 sender.selectedSegmentIndex 是目前選中的電影索引。通過將它賦值給 index,我們將 index 設置為目前選中的電影索引。這樣,我們就可以根據這個索引來顯示目前選中的電影的海報和文本內容。
  • 實際上,當使用者點擊選項卡時,UISegmentedControl 會自動更新其 selectedSegmentIndex 屬性。在這個 IBAction 中,我們只是將這個屬性的值賦值給了 index。
   
@IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) {

// Segmented Control 當前頁面
index = sender.selectedSegmentIndex

// movie封面
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)

// movie文本內容
movieContentTextView.text = moviesDescription[index]

}

Segmented Control 與 Button、Page Control 連動

Segmented Control未與其他切換功能連動
  • Segmented Control 也是有欄位顯示當前位置,所以必須將其與其他功能連動。
  • selectedSegmentIndex屬性表示當前被選中的分段的索引。在程式碼中將 index 賦值給 selectedSegmentIndex 時,它會將 UISegmentedControl 選中的分段更新索引為 index 的分段,從而將選中的分段與當前顯示的電影對應起來。
  • movieSegmentedControl.selectedSegmentIndex = index 放至其他切換的IBAction中做連動。
    @IBAction func goToNextPage(_ sender: Any) {
// Button 下一頁 (movies.count = 3)
index = (index + 1) % movies.count
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)
movieContentTextView.text = moviesDescription[index]

// pageControl 的 outlet 連動
pageControl.currentPage = index

// SegmentedControl 的 outlet 連動
movieSegmentedControl.selectedSegmentIndex = index
}

@IBAction func goToPreviousPage(_ sender: Any) {
// Button 上一頁
index = (index + movies.count - 1) % movies.count
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)
movieContentTextView.text = moviesDescription[index]

// pageControl 的 outlet 連動
pageControl.currentPage = index

// segmentedControl 的 outlet 連動
movieSegmentedControl.selectedSegmentIndex = index
}

@IBAction func pageControlDidChange(_ sender: UIPageControl) {
// pageControl 當前頁面
index = sender.currentPage
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)
movieContentTextView.text = moviesDescription[index]

// segmentedControl 的 outlet 連動
movieSegmentedControl.selectedSegmentIndex = index
}

@IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) {
// Segmented Control 當前頁面
index = sender.selectedSegmentIndex
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)
movieContentTextView.text = moviesDescription[index]

// pageControl 的 outlet 連動
pageControl.currentPage = index

}

function運用

  • 後來我發現有幾個程式碼功能重複使用性很高,所以就試著將其整理成一個function,updateMovieInformation()
  • 包含封面、文本的切換,以及pageControl、segmentedControl的連動顯示。
  • 將這個 function 替代原先密密麻麻的程式碼,同時在測試時也相對方便@@
    // 設置一個 function: 關於圖片文本切換
func updateMovieInformation() {

// movie封面
let moviesName = movies[index]
movieCoverImageView.image = UIImage(named: moviesName)

// movie文本內容
movieContentTextView.text = moviesDescription[index]

// pageControl 的 outlet 連動
pageControl.currentPage = index

// segmentedControl 的 outlet 連動
movieSegmentedControl.selectedSegmentIndex = index
}

@IBAction func goToNextPage(_ sender: Any) {
// Button 下一頁 (movies.count = 3)
index = (index + 1) % movies.count

updateMovieInformation()

}

@IBAction func goToPreviousPage(_ sender: Any) {
// Button 上一頁
index = (index + movies.count - 1) % movies.count

updateMovieInformation()
}

@IBAction func pageControlDidChange(_ sender: UIPageControl) {
// pageControl 當前頁面
index = sender.currentPage

updateMovieInformation()
}

@IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) {
// segmentedControl 當前選項
index = sender.selectedSegmentIndex

updateMovieInformation()
}

}

Swipe Gesture

手勢滑動
  • 設置左滑、右滑會切換頁面的手勢。(要注意我當初設反XD)
  • 將Swipe Gesture Recognizer拉到UIImageView上,屆時滑動照片即可觸發。
  • 設置其Swipe滑動方向,以及觸碰手勢。
  • 一個Swipe Gesture Recognizer只能辨識一個方向,因此我設置了兩個分為左右。

User Interaction Enabled選項

  • 讓 image view 觸碰有反應,藉此可以辨識Swipe手勢。

連結手勢的 IBAction

  • 先確定 Swipe Gesture 有與 UIImageView 連動。
  • 接著在將 Swipe Gesture 拉至 IBAction 的 function 設定滑動時會觸發的功能。
  • 這邊我想要偵測到左滑時顯示下一頁的內容,所以將 Swipe Gesture 連結到NextPageButton的IBAction。
  • 也因為NextPageButton的IBAction原先就已經被放置updateMovieInformation() ,所以在Swipe時這些功能也會觸發。

請ChatGPT補充

  • Swipe Gesture與Button的差異性

Long Press Gesture Recognizer

用長按的手勢來搭配先前學到的亂數運用。

  • 長按手勢的時間我在Attribute Inspector設置1秒。
  • 不過我寫完之後用print檢查發現,長按一下螢幕時,它會生成不固定數量的亂數,並更新電影頁面。
    @IBAction func changeImageOnLongPress(_ sender: Any) {

// movies.count 的值為 3,因此要使用..<
let number = Int.random(in: 0..<movies.count)

index = number

updateMovieInformation()

print(index)
}
  • 如下圖XD

詢問Peter之後才知道這是跟目前還沒學到的判斷狀態的運用有關聯,雖然這邊還不了解,但先備註。

  • 先將sender改為 UILongPressGestureRecognizer ,這樣才可以直接存取它的屬性和方法,使得長按事件可以正確地觸發。
  • sender 是指觸發事件的物件,這邊指的是 UILongPressGestureRecognizer 物件。在 senderstate 屬性中,記錄了目前手勢的狀態。當手勢的狀態變為 .ended 時,表示手指已經離開螢幕,也就是長按的動作已經完成了。
  • 因此這段程式碼的意思是當長按手勢完成(也就是手指離開螢幕)時,才會執行裡面的程式碼。
    @IBAction func changeImageOnLongPress(_ sender: UILongPressGestureRecognizer) {

// 狀態判斷
if sender.state == .ended {
let number = Int.random(in: 0..<movies.count)

index = number

updateMovieInformation()

print(index)
}

}
  • 修改過後:當長按手勢離開螢幕時就會執行程式碼,並且只會產生一個亂數!

GitHub

參考

--

--

wei Tsao 學習紀錄
彼得潘的 Swift iOS / Flutter App 開發教室

Hi ! 我是wei , 先前未接觸過程式開發設計,想藉此來記錄自己的學習歷程,以利培養自己的程式邏輯 :)