⑤⑦ 電影中的月光

運用 page control,segmented control,button & gesture 更換內容,使用 WKWebView 播放 YouTube 影片

Min
彼得潘的 Swift iOS / Flutter App 開發教室
31 min readSep 6, 2023

--

電影中很常出現古典音樂,無論是畫外音,還是畫內音。貝多芬的月光奏鳴曲第一樂章因為非常家喻戶曉,又很適合許多電影的情境(?)所以是電影中的常客。

沒想到一估狗發現還真不是普通的多,根據以下網站記載,至少有 66 部電影裡都使用月光奏鳴曲(還不包括柯南XD),而且,網站裡的資料還不是全部。

https://beta.icheckmovies.com/lists/7047-films+with+beethovens+moonlight+sonata+in+it?showall=true

今天的 App 收錄有人演奏月光奏鳴曲的幾部電影,也把演奏片段放在裡面。

永遠的愛人(1994)

蓋瑞歐德曼飾演的貝多芬本人演奏。

大象(2003)

這段男主角(算吧唉)彈琴畫面一開始演奏給愛麗絲,3:37開始是月光。

大象這段用了第一樂章整首沒有剪輯,但是是純配樂。(不放 App 裡)

攔截記憶碼(2012)

柯林法洛彈了月光又彈了暴風雨,然後就發現秘密了。

史努比電影(2015)

當然是謝樂德彈的囉!

咳咳…我們趕快回到 Xcode 裡吧!今天的專案是各種翻頁的練習並使用WKWebView 播放 YouTube 裡的音樂片段。先設定 page control、segmented control、button 三種方式翻頁,再點擊骰子亂數翻頁。最後加入使用手勢 (gesture) 的方式左右翻頁與長按亂數翻頁。

設定 viewDidLoad

先設定好版面,並連接 IBOutlet 與 IBAction(手勢等最後再放)。與此同時也將 WKWebView 連接 IBOutlet。

WKWebView

先在 view 上面新增一個 WKWebView,並設定好位置大小,然後跟其他元件一樣,連接 IBOutlet。

import WebKit // 使用 WKWebView 需要先輸入 WebKit

class ViewController: UIViewController {

// 顯示 YouTube 電影連結
@IBOutlet weak var movieWebView: WKWebView!
來貘配色

因為第一頁就有永遠的愛人的連結,因此先製作第一個 YouTube連結。影片網址如下(不要點選 YouTube 網頁的分享,直接複製網址列的內容):

https://www.youtube.com/watch?v=524VlYD0PVw

v= 之後為影片 id,加到這連結之後:

https://www.youtube.com/embed/

組合成可內嵌的網址:

https://www.youtube.com/embed/524VlYD0PVw

在 viewDidLoad中設定如下:

// 創建一個URL對象,用於指定網頁的URL地址
let url = URL(string: "https://www.youtube.com/embed/524VlYD0PVw")!
// 使用URL來創建一個URLRequest對象,這個請求將用於WKWebView
let request = URLRequest(url: url)
// 使用WKWebView的load方法來加載指定的URLRequest,顯示網頁內容
movieWebView.load(request)

目前程式:

import UIKit
import WebKit // 使用 WKWebView 需要先輸入 WebKit

class ViewController: UIViewController {

// 顯示 YouTube 電影連結
@IBOutlet weak var movieWebView: WKWebView!
// segmentedControl
@IBOutlet weak var segmentedControl: UISegmentedControl!
// 電影海報
@IBOutlet weak var moviePicImageView: UIImageView!
// 電影名稱
@IBOutlet weak var movieNameLabel: UILabel!
// Page Control
@IBOutlet weak var pageControl: UIPageControl!

// 將電影海報放進陣列
let moviesPic = ["Immortal Beloved","Elephant","Total Recall","The Peanuts Movie"]
// 將電影名稱放進陣列
let movieName = ["永遠的愛人", "大象", "攔截記憶碼", "史努比電影"]
// 將 YouTube 網址放進陣列
let youtubeURL = ["https://www.youtube.com/embed/524VlYD0PVw","https://www.youtube.com/embed/F5YNsoh1L6M","https://www.youtube.com/embed/CqDkDyA7QHE","https://www.youtube.com/embed/dKA-NOQBdH0"]
// 計算陣列的 index 先預設為 0
var index = 0

override func viewDidLoad() {
super.viewDidLoad()
// 第一部電影是永遠的愛人,因此先放他的電影海報
moviePicImageView.image = UIImage(named: "Immortal Beloved")
// 先放電影名稱
movieNameLabel.text = "永遠的愛人"
// App 點進來看到所有的內容都是第一頁,因此 segmentedControl 與 pageControl 都設在 0
segmentedControl.selectedSegmentIndex = 0
pageControl.currentPage = 0
// YouTube 影片播放設定
// 創建一個URL對象,用於指定網頁的URL地址
let url = URL(string: "https://www.youtube.com/embed/524VlYD0PVw")!
// 使用URL來創建一個URLRequest對象,這個請求將用於WKWebView
let request = URLRequest(url: url)
// 使用WKWebView的load方法來加載指定的URLRequest,顯示網頁內容
movieWebView.load(request)

}

// 點右鍵 buttun 往後一部電影
@IBAction func turnRight(_ sender: UIButton) {
}
// 點左鍵 buttun 往前一部電影
@IBAction func turnLeft(_ sender: UIButton) {
}
// segmentedControl
@IBAction func choosePage(_ sender: UISegmentedControl) {
}
// 點骰子可以隨機選電影
@IBAction func randomChoosePage(_ sender: UIButton) {
}
// 點 Page Control 可以跳到前一部或後一部電影
@IBAction func clickPageControl(_ sender: UIPageControl) {
}
}

因為有不只一部電影,因此將所有電影海報的圖片、名稱與網址先放在陣列裡,並且將稍後要用來提取三者的 index 宣告為 0 的變數。

    // 將電影海報放進陣列
let moviesPic = ["Immortal Beloved","Elephant","Total Recall","The Peanuts Movie"]
// 將電影名稱放進陣列
let movieName = ["永遠的愛人", "大象", "攔截記憶碼", "史努比電影"]
// 將 YouTube 網址放進陣列
let youtubeURL = ["https://www.youtube.com/embed/524VlYD0PVw","https://www.youtube.com/embed/F5YNsoh1L6M","https://www.youtube.com/embed/CqDkDyA7QHE","https://www.youtube.com/embed/dKA-NOQBdH0"]
// 計算陣列的 index 先預設為 0
var index = 0

點開模擬器,檢查是不是出現 viewDidLoad 的內容:

成功放入 YouTube 影片
影片點擊自動變全螢幕播放,太感動啦!

翻頁

翻頁要做的事情有

  1. 更換電影海報
  2. 更換電影名稱
  3. 更換 YouTube 連結
  4. 將 Segmented Control 的頁籤換到相同電影名稱上
  5. 將 Page Contorl 的小圓點移到對應的位置

向右向左 Button

          // 點右鍵 buttun 往後一部電影
@IBAction func turnRight(_ sender: UIButton) {
// 每點選一次 index 就會 +1,直到數字可被總電影數(4)整除,就會將 index 變成 0 ,回到第一部
index = (index + 1) % moviesPic.count
// 帶入陣列中相應 index 的海報、名稱與 YouTube 連結
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

// 輸入海報、名稱
moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
// 輸入 YouTube 連結並載入
let request = URLRequest(url: url)
movieWebView.load(request)

// Page Control 切換到當頁圓點
pageControl.currentPage = index
// Segmented Control 切換到當頁頁籤
segmentedControl.selectedSegmentIndex = index


}
// 點左鍵 buttun 往前一部電影
@IBAction func turnLeft(_ sender: UIButton) {
// moviesPic.count - 1 = 3 ,每點選一次 index 就會 + 3,index 變成數字被總電影數(4)整除後的餘數
index = (index + moviesPic.count - 1) % moviesPic.count
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = index
segmentedControl.selectedSegmentIndex = index
}

寫向右向左 Button 最重要的是怎麼知道按下去後到第幾頁。因為像 Page Control 跟 Segmented Control 都有固定的頁數,指定 index 即可。向右向左 Button 需要一點運算,加上求餘數的幫忙:

index = (index + 1) % moviesPic.count

向右 Button 的邏輯:點一次 index 就會 +1,到 index + 1 可以整除電影的總數(4部)時,index 會回到 0 也就是第一部。這樣可以一直輪迴。

index = (index + moviesPic.count - 1) % moviesPic.count

向左 Button 的邏輯:因為 0 減 1 會變成負數,對於求餘數沒有幫助,因此先加 4(電影總數),然後再計算減 1 就不會變成負數了。

驗算,當本來 index 為 0 :

index = (0 + 4 - 1) % 4 // index 為 3
// 計算答案為3,因此會跳到第四部電影,也就是最後一部。

本階段成果 — 點選往右 Button:

本階段成果 — 點選往左 Button:

Page Control

@IBAction func clickPageControl(_ sender: UIPageControl) {
// sender 將點的動作傳給 index,currentPage 看是往前點還是往後點,改變 index
index = sender.currentPage
let name = movieName[index]
let pic = moviesPic[index]
let url = URL(string: youtubeURL[index])!

movieNameLabel.text = name
moviePicImageView.image = UIImage(named: pic)
let request = URLRequest(url: url)
movieWebView.load(request)

segmentedControl.selectedSegmentIndex = index
}

整體跟 Button 很像,除了 index 的取得方法之外,最後不用再改 Page Control 的顯示,因為點下去的時候它已經自己改了,只要連動 Segmented Control 的頁籤就好。

index = sender.currentPage

因為在連接 IBAction 的時候,將 Type 更改為 UIPageControl,因此使用 sender.currentPage 可感知當前使用者點選的圓點是往前還是往後的頁面。將此數據傳給 index 之後,就可以用來改變其他物件了。

本階段成果 — 點選圓點:

Segmented Control

        @IBAction func choosePage(_ sender: UISegmentedControl) {
// sender 將點的動作傳給 index,selectedSegmentIndex 看選了哪個頁籤,改變 index
index = sender.selectedSegmentIndex
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = index
}

Segmented Control 跟 Page Control 道理相仿,只是在接收當前 index 的時候, Page Control 用的是 currentPage,Segmented Control 用的是 selectedSegmentIndex

本階段成果 — 點選 Segmented Control:

點骰子隨機選頁

這邊要注意兩件事情,第一個是 Random,第二個是不與當前重複。因為如果亂數出來是當前頁面,就沒有效果了。

彼得潘提示第二項要用 while 解決,這邊的邏輯應該是:

當亂數的數字與原本的 index 不同,才執行變化。亂數的數字與原本的 index 相同,則再骰一次。直到數字不同。

我第一次寫的程式,模擬器出來,點擊骰子的確有亂數,但偶而會重複:

// 點骰子可以隨機選電影
@IBAction func randomChoosePage(_ sender: UIButton) {
var indexNew = index
while indexNew == index {
indexNew = Int.random(in: 0...3)
}

let pic = moviesPic[indexNew]
let name = movieName[indexNew]
let url = URL(string: youtubeURL[indexNew])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = indexNew
segmentedControl.selectedSegmentIndex = indexNew

}

看出來為什麼了嗎?

原來是我在 while 後面,沒有再把亂數出來的 indexNew 放回 index 裡,這樣 index 還是保持原來的數字,下一次再按骰子的時候,indexNew 的數字又變回 index 原先的值,那如果這麼巧再一次 Random 出來的 indexNew 與上一次 indexNew 是同個數字,程式也只知道跟這次原先的 index 不一樣,那就不會改變了(因為還是變成與目前畫面相同的畫面)。

如果我的文字敘述很繞…請看簡報講解:

其實只要在 while 之後將 index 更新為 indexNew,就能確保不會重複了!

正確寫法:

  // 點骰子可以隨機選電影
@IBAction func randomChoosePage(_ sender: UIButton) {
var indexNew = index
while indexNew == index {
indexNew = Int.random(in: 0...3)
}
index = indexNew // 加這行就好!
let pic = moviesPic[indexNew]
let name = movieName[indexNew]
let url = URL(string: youtubeURL[indexNew])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = indexNew
segmentedControl.selectedSegmentIndex = indexNew
}

本階段成果 — 點選骰子:

Swipe Gesture 左滑右滑

先拜讀一下:

第一步,將 Swipe Gesture Recognizer 加到電影海報的 Image View 中:

可以看到多了 Swipe Gesture Recognizer 在 Document Outline 中:

也可以右鍵 Swipe Gesture Recognizer 確定連接的關係:

將 Image View 的 User Interaction Enabled 勾選,它就能參與互動了:

因為一個 Swipe Gesture Recognizer 只能設定一種手勢,我們先設定右滑的,之後再創建另一個 Swipe Gesture Recognizer 給左滑。點擊 Swipe Gesture Recognizer 在 Swipe 選擇 Right:

連結 IBAction,有不同的方法可以使用。我這次使用兩個 Swipe 都連到同一個 IBAction funtion。

結果我寫出來以後,發現我以為的右滑其實是左滑,左滑其實是右滑!?原來左轉出去是右滑,右轉是左滑。如果多個字:向右滑、向左滑可能就比較不會錯亂了。

本階段成果 — 左滑右滑:

長按圖片隨機跳頁

與 Swipe 一樣,Long Press Gesture Recognizer 也是加入海報 Image View 之後會出現在 Document Line:

因為已經指定了長按,因此不用選擇方向。其後設定皆相同,只是在程式裡要設定長按的時間,亂數則與按骰子方法一樣:

@IBAction func longPressRandomChangePage(_ sender: UILongPressGestureRecognizer) {
// 設定最短長按時間(秒)
sender.minimumPressDuration = 3
var indexNew = index
while indexNew == index {
indexNew = Int.random(in: 0...3)
}
let pic = moviesPic[indexNew]
let name = movieName[indexNew]
let url = URL(string: youtubeURL[indexNew])!

index = indexNew // 更新 index
moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = indexNew
segmentedControl.selectedSegmentIndex = indexNew
}

本階段成果 — 長按隨機跳頁:

到這邊預計要做的功能都完成了,這篇作業的幾個進階內容,Pan Gesture Recognizer、用 struct 定義資料型別、利用 Timer 自動輪播內容之後再結合其他作業一起實現。

我用 ipad 螢幕錄影完成影片,本來只是想試試看全螢幕播電影XD 結果發現離開 App 後電影還在小螢幕播放!

Magic!

總之很驚喜的結束了,謝謝收看。

製作時長: 7 小時

全部程式:

import UIKit
import WebKit // 使用 WKWebView 需要先輸入 WebKit

class ViewController: UIViewController {

// 顯示 YouTube 電影連結
@IBOutlet weak var movieWebView: WKWebView!
// segmentedControl
@IBOutlet weak var segmentedControl: UISegmentedControl!
// 電影海報
@IBOutlet weak var moviePicImageView: UIImageView!
// 電影名稱
@IBOutlet weak var movieNameLabel: UILabel!
// Page Control
@IBOutlet weak var pageControl: UIPageControl!
// 將電影海報放進陣列
let moviesPic = ["Immortal Beloved","Elephant","Total Recall","The Peanuts Movie"]
// 將電影名稱放進陣列
let movieName = ["永遠的愛人", "大象", "攔截記憶碼", "史努比電影"]
// 將 YouTube 網址放進陣列
let youtubeURL = ["https://www.youtube.com/embed/524VlYD0PVw","https://www.youtube.com/embed/F5YNsoh1L6M","https://www.youtube.com/embed/CqDkDyA7QHE","https://www.youtube.com/embed/dKA-NOQBdH0"]
// 計算陣列的 index 先預設為 0
var index = 0

override func viewDidLoad() {
super.viewDidLoad()
// 第一部電影是永遠的愛人,因此先放他的電影海報
moviePicImageView.image = UIImage(named: "Immortal Beloved")
// 先放電影名稱
movieNameLabel.text = "永遠的愛人"
// App 點進來看到所有的內容都是第一頁,因此 segmentedControl 與 pageControl 都設在 0
segmentedControl.selectedSegmentIndex = 0
pageControl.currentPage = 0
// YouTube 影片播放設定
// 創建一個URL對象,用於指定網頁的URL地址
let url = URL(string: "https://www.youtube.com/embed/524VlYD0PVw")!
// 使用URL來創建一個URLRequest對象,這個請求將用於WKWebView
let request = URLRequest(url: url)
// 使用WKWebView的load方法來加載指定的URLRequest,顯示網頁內容
movieWebView.load(request)
}

// 點右鍵 buttun 往後一部電影
@IBAction func turnRight(_ sender: UIButton) {
// 每點選一次 index 就會 +1,直到數字可被總電影數(4)整除,就會將 index 變成 0 ,回到第一部
index = (index + 1) % moviesPic.count
// 帶入陣列中相應 index 的海報、名稱與 YouTube 連結
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

// 輸入海報、名稱
moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
//輸入 YouTube 連結並載入
let request = URLRequest(url: url)
movieWebView.load(request)

// Page Control 切換到當頁圓點
pageControl.currentPage = index
// Segmented Control 切換到當頁頁籤
segmentedControl.selectedSegmentIndex = index
}
// 點左鍵 buttun 往前一部電影
@IBAction func turnLeft(_ sender: UIButton) {
// moviesPic.count - 1 = 3 ,每點選一次 index 就會 + 3,index 變成數字被總電影數(4)整除後的餘數
index = (index + moviesPic.count - 1) % moviesPic.count
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = index
segmentedControl.selectedSegmentIndex = index
}
// segmentedControl
@IBAction func choosePage(_ sender: UISegmentedControl) {
// sender 將點的動作傳給 index,selectedSegmentIndex 看選了哪個頁籤,改變 index
index = sender.selectedSegmentIndex
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = index
}
// 點骰子可以隨機選電影
@IBAction func randomChoosePage(_ sender: UIButton) {
var indexNew = index
while indexNew == index {
indexNew = Int.random(in: 0...3)
}
let pic = moviesPic[indexNew]
let name = movieName[indexNew]
let url = URL(string: youtubeURL[indexNew])!

index = indexNew // 更新 index
moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = indexNew
segmentedControl.selectedSegmentIndex = indexNew
}

// 點 Page Control 可以跳到前一部或後一部電影
@IBAction func clickPageControl(_ sender: UIPageControl) {
// sender 將點的動作傳給 index,currentPage 看是往前點還是往後點,改變 index
index = sender.currentPage
let name = movieName[index]
let pic = moviesPic[index]
let url = URL(string: youtubeURL[index])!

movieNameLabel.text = name
moviePicImageView.image = UIImage(named: pic)
let request = URLRequest(url: url)
movieWebView.load(request)

segmentedControl.selectedSegmentIndex = index
}

@IBAction func swipeChangePage(_ sender: UISwipeGestureRecognizer) {
if sender.direction == .left{
index = (index + 1) % moviesPic.count
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)

pageControl.currentPage = index
segmentedControl.selectedSegmentIndex = index

} else if sender.direction == .right{
index = (index + moviesPic.count - 1) % moviesPic.count
let pic = moviesPic[index]
let name = movieName[index]
let url = URL(string: youtubeURL[index])!

moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)

pageControl.currentPage = index
segmentedControl.selectedSegmentIndex = index

}
}

@IBAction func longPressRandomChangePage(_ sender: UILongPressGestureRecognizer) {
// 設定最短長按時間(秒)
sender.minimumPressDuration = 3
var indexNew = index
while indexNew == index {
indexNew = Int.random(in: 0...3)
}
let pic = moviesPic[indexNew]
let name = movieName[indexNew]
let url = URL(string: youtubeURL[indexNew])!

index = indexNew // 更新 index
moviePicImageView.image = UIImage(named: pic)
movieNameLabel.text = name
let request = URLRequest(url: url)
movieWebView.load(request)
pageControl.currentPage = indexNew
segmentedControl.selectedSegmentIndex = indexNew
}
}

GitHub:

Reference:

--

--