利用 page control, segmented control, button, & gesture 切換圖片內容

連接起StoryBoard和程式的IBOutlet & IBAction

--

StoryBoard的元件設計&程式碼就像是兩個世界互不相干,
要讓兩邊產生關聯達到相輔相成的作用,就要在兩者之間建立IBOutletIBAction。

什麼是IBOutlet &IBAction

IBOutlet

要從程式修改Storyboard上的元件,以及在IBAction中會引用到該元件時,要透過拉IBOutlet產生變數。

@IBOutlet weak var volumeSlider: UISlider!

IBAction

要觸發Storyboard上某元件的event,需透過拉IBAction產生function。例如:使用者點選按鈕後,才會觸發AVfoundation的說話指令。而像ImageView和Label這類純展示內容的元件則無法拉IBAction

@IBAction func adjustVolume(_ sender: UISlider)

sender的型別可以是Any,也可以是元件本身,如果在function中會引用到元件時可以將型別改成元件本身。例如需要存取UISlider的value給說話的音量時,sender的型別就要設成UISlider後才能被存取。
或是前面友邦UISlider拉過IBOutlet的情況下,也可以寫成speechUtterence.volume = Float(volumeSlider.value)

@IBAction func adjustVolume(_ sender: UISlider) {speechUtterence.volume = Float(sender.value)}

如何拉 IBOutlet &IBAction

選擇右上角第二個icon>打開 Assistant editor

IBOutlet和IBAction的程式都會寫在class的裡面。
Class類別中能寫的程式只有property (以變數宣告) 和 method (以function宣告),習慣上class的上半部會放變數、下半部會放function/method

以func viewDidLoad()為分界,從StoryBoard上點選元件按著右鍵拉到func viewDidLoad() 以上會自動判定拉的是IBOutlet、以下是IBAction

class ViewController: UIViewController {    
@IBAction func adjustVolume(_ sender: UISlider)

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func adjustVolume(_ sender: UISlider){
}
}

製作時遇到的問題

基本上peter提醒需特別注意的地方,我在重新練習時全部都碰上了一次orz,所以筆記在文章前面

  • 如果要幫IBOutlet和IBAction改名,直接在Assistant editor裡改名會導致程式死掉,所以可以最好刪掉重新建新的IBOutlet和IBAction,
    或是改名後到connections inspector把原本events刪掉再重新拉一次線。
  • 多個元件可以連同一個IBAction
  • 如果要複製一個已經拉了IBAction的元件時,該元件的IBAction也會一起複製到新元件上 (IBOutlet不會有這個狀況)。所以要記得檢查Send Events的連線是否符合新元件的action。
  • 拉完IBAction的Function不用特別呼叫,而是依賴使用者在app中與元件互動來執行event和action
  • 使用者與元件互動一次=執行一次event=跑一次該元件連上的IBAction = 執行一次該IBAction的function下的程式

檢查IBOutlet和IBAction的連線狀況有兩種方法:
1. 從Xcode介面右側點選connections inspector
2. 從Xcode介面左側元件列表對元件點選右鍵

製作介紹:用不同元件切換ImageView裡的圖片

我要透過一組按鈕、頁面圓點、分段按鈕、手勢 (手機中可用手指滑動或電腦滑鼠觸控板操作)來切換小小兵的圖片。

1. 按button換圖片

StoryBoard

在StoryBoard中新增一個ImageView,同時拉圖片進Assets中。以下我拉了四隻小小兵的圖片。再新建兩個切換下一張、上一張圖片的UIButton。

程式碼

為button設定一個屬性叫minions,裡面存一個array包含4個小小兵的名字。再設定一個index變數,記錄現在是第幾個圖片。index = 0指的就是array裡的第一個值“Stuart”

之後每點一下按紐(=觸發一次event)就會跑一次function。直覺上function會執行index+1切換到下一張圖,但實際上持續點按紐後,index 會從minions[0]…minions[1]..minions[2]到最後的minions[3],但沒有第五隻小小兵(minions[4])的情況下,繼續index+1就會爆掉。
所以要用(index + 1) % minions.count(index + 1)除以小小兵名字的數量後得到的餘數,來避免數字爆掉使程式死掉,做到圖片無限循環的效果。

切換下一張(index + 1) % minions.count

var index = 0(0 + 1) % 4 的餘數 = 1  //index = 1 → minions[1]
(1 + 1) % 4 的餘數 = 2 //index = 2 → minions[2]
(2 + 1) % 4 的餘數 = 3 //index = 3 → minions[3]
(3 + 1) % 4 的餘數 = 0 //index = 4 → minions[0]
(4 + 1) % 4 的餘數 = 1 //index = 1 → minions[1]

切換上一張(index + minions.count — 1) % minions.count

var index = 0(0 + 4 - 1) % 4 = 3    //index = 3 → minions[3]
(3 + 4 - 1) % 4 = 2 //index = 2 → minions[2]
(2 + 4 - 1) % 4 = 1 //index = 1 → minions[1]
(1 + 4 - 1) % 4 = 0 //index = 0 → minions[1]

最後用minions[index]讀出小小兵名字(讀出來的型別是String),
將他存入常數name
再將minionImageView要顯示的圖片內容UIImage傳入。

@IBOutlet weak var minionImageView: UIImageView!
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var preButton: UIButton!
let minions = [“Stuart”,”Bob”,”Jerry”,”Mel”]
var index = 0
// 壓按鈕切換下一張
@IBAction func nextImage(_ sender: UIButton) {
index = (index + 1) % minions.count
let name = minions[index]
print(index,name) //在debugger area中驗證是否正確
minionImageView.image = UIImage(named: name)
}
// 壓按鈕切換上一張
@IBAction func preImage(_ sender: UIButton) {
index = (index + minions.count - 1) % minions.count
let name = minions[index]
print(index,name) //在debugger area中驗證是否正確
minionImageView.image = UIImage(named: name)
}

2. 用手勢切換圖片

StoryBoard

從Library新增一個gesture,因為要用手指左右滑動換圖片,所以添加兩個Swipe Gesture Recognizer (一個當作左滑、一個右滑),把它拖到小小兵的ImageView上。
ImageView平時只顯示圖片,所以要讓使用者在圖上滑動,要勾選User Interaction Enabled,允許使用者與元件互動。
最後回到Swipe Gesture Recognizer,在Attributes inspector選擇滑動的方向,將向左滑動當作下一頁、向右滑動是下一頁。

程式碼

Swipe Gesture Recognizer的event和按鈕一樣都是換圖片,只差在一個手滑動一個點按鈕,所以IBAction可以連接到同一個。但要把前面按鈕的sender 型別設成Any func nextImage(_ sender: Any) func preImage(_ sender: Any)

3. 用PageControl 和 SegmentedControl切換圖片

StoryBoard

在Canvas上新增一個UIPageControl 和 SegmentedControl ,可以先在Attributes Inspector裡設置元件的屬性

程式碼

接下來要用程式實現:
1. 在app中點擊PageControl時,執行讓小小兵圖片切換的action,
並且SegmentedControl的segment必須對應到PageControl的圓點同時切換
2. 在app中點擊SegmentedControl的segment(小小兵名字)時,執行讓小小兵圖片切換的action,
並且PageControl的圓點必須對應到SegmentedControl的segment同時切換

為什麼SegmentedControl的segment和PageControl的圓點要寫兩次?

因為使用者點擊元件,就是觸發該元件的 event,其他沒有互動的元件就不會發生event更不會執行後續的action

所以在寫一個IBAction要執行的程式時,要站在那個元件的角度思考,當元件被使用者操作,例如點擊、滑動等(=event被觸發),會希望畫面上有什麼其他的功能被執行?

恩..有觸發NPC後引發一堆任務的感覺…

回到例子上,如果點擊的是PageControl,那SegmentedControl IBAction裡的內容就不會執行,所以兩邊都要寫。同理~如果壓上下頁的按鈕時也要讓PageControl 和SegmentedControl同步動作,在button 的function下就要記得把他們的動作寫進去。

現在來切換圖片。PageControl有個屬性叫currentPage,第一頁的值是0、第二頁的值是1….以此類推,把它存給變數index,來控制圖片切換。SegmentedControl則是用屬性selectedSegmentIndex,默認是未選取的狀態,數值是 -1。第一段的值是0、第二段的值是1….以此類推,同樣把它存給變數index,來控制圖片切換。

最後用minions[index]讀出小小兵名字(讀出來的型別是String),將他存入常數name ,再將minionImageView要顯示的圖片內容UIImage傳入。

@IBOutlet weak var minionPageControl: UIPageControl!
@IBOutlet weak var nameSegmentedControl: UISegmentedControl!
let minions = ["Stuart","Bob","Jerry","Mel"]
var index = 0
//點擊分頁控制後執行動作
@IBAction func pageControlTapped(_ sender: UIPageControl) {

index = sender.currentPage
let name = minions[index]
minionImageView.image = UIImage(named: name)
//從分頁控制切換圖片時,讓分段按鈕對應著切換
nameSegmentedControl.selectedSegmentIndex = index
}

//點擊分段控制後執行動作
@IBAction func segmentedControlTapped(_ sender: UISegmentedControl) {
index = sender.selectedSegmentIndex
let name = minions[index]
minionImageView.image = UIImage(named: name)
//從分段按鈕選名字時,讓分頁控制對應著切換
minionPageControl.currentPage = index
}

成品

完整程式碼

專案下載

想知道小小兵的說話器的製作說明請見下一篇文章
小小兵的AVFoundation之AVSpeechSynthesizer應用篇~

--

--