UISplitViewController簡單入門
在郵件這個App裡,它在iPad裡劃分了兩個區域,左邊是一個郵件列表,右邊則是對應的郵件詳細內容. Apple為我們建立了一個非常方便的ViewController,它的名字叫做UISplitViewController. 在這個教程中,我們將學習如何去使用它,還有一個事情就是,在iOS 8開始,UISplitViewController就可以在iPad和iPhone上執行.
在本教程中,我們將會從頭開始建立一個通用的App,它使用UISplitViewController來顯示水果的列表,我們將使用UISplitViewController來處理iPad和iPhone 11的Navigation
在此之前,你應該掌握iOS開發的一些基礎知識,比如AutoLayout和Storyboard的使用等等.
在開始之前,我們需要下載本教程的一些課件,這裡的課件共有兩個,一個是已經完成了的,一個是準備讓你去完成的.
注意: 這裡使用的是Xcode 11,iOS 13和Swift 5,如需轉載,請聯絡作者,侵權必究.
開始
點選File ▸ New ▸ Project,在Xcode中建立一個新的專案,選擇iOS ▸ Application ▸ Single View App模板
將專案命名為Fruit,將開發語言設定為Swift,然後將使用者介面設定為Storyboard
雖然我們可以選擇使用Master-Detail App這個模板,但是為了更好的瞭解UISplitViewController的工作原理,所以我們將使用Single View App模板. 這對我們在將來的專案使用UISplitViewController時會更有幫助.
現在我們來建立App的主體UI,開啟Main.storyboard,這裡我們需要把系統自帶的ViewController刪除,同時也要將專案中的ViewController.swift檔案也刪掉.
然後在Main.storyboard中找到Split View Controller
這裡會讓Storyboard新增幾個元素:
-
Split View Controller: 第一個肯定是UISplitViewController,它將是這個App的根控制器.
-
Navigation Controller: 其次是UINavigationController,它將是主控制器的根檢視,在iPad或者是比較大尺寸的iPhone橫屏時,它將會顯示在左側.
仔細點檢視,在UISplitViewController中,這個具有UINavigationController的控制器,是它的Master View Controller,這將允許我們在主檢視控制器中建立整個Navigation的層次結果,然後又不影響到Detail View Controller.
-
View Controller: 這裡將會顯示所有水果的詳細資訊,如果你仔細點檢視UISplitViewController,你會發現ViewController是它的Detail View Controller.
- Table View Controller: 這是UINavigationController的根檢視,它將會顯示水果列表.
注意: 這個時候由於我們沒有Cell的重用標識,所以Xcode會有個警告,這個前往別忘記了.
還有一點要注意的是,由於我們把自帶的ViewController給刪除了,所以我們需要告訴Storyboard,我們希望將UISplitViewController設定為初始化ViewController.
這個時候我們選擇UISplitViewController,然後在右側的"屬性"欄,勾選Is Initial View Controller:
勾選了之後,我們就會在UISplitViewController的左側看到一個箭頭,這就是這個Storyboard的初始化控制器.
這個時候,我們選擇iPad模擬器,然後將模擬器橫向後,你就會看到下面這個空白的UISplitViewController了:
之前也說了,自從iOS 8之後,我們也可以執行在iPhone上,只要它的尺寸夠大就可以了,這裡我們選擇iPhone 8 Plus模擬器,然後就會看到效果:
在橫向的大尺寸iPhone會和iPad顯示的效果一樣之外,UISplitViewController將會和常規的操作一樣,會有UINavigationController的Push和Pop,這都是系統幫我們實現的,不需要我們而外再去操作,
建立自定義ViewController
現在我們已經有了Storyboard主要的控制器結構,現在我們需要的是在程式碼上新增資料來源,然後將資料顯示出來.
現在我們建立一個名為MasterViewController的UITableViewController子類,在建立的過程裡,我們需要把Also create XIB file給去掉,因為在Storyboard裡已經有了,然後開發語言為Swift,然後就一直下一步,到最後完成建立即可.
建立完成了之後,我們開啟MasterViewController.swift,刪除一些不需要的程式碼,然後新增一些我們需要的程式碼:
override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FruitCell",for: indexPath)
return cell
}
複製程式碼
在執行之前,我們需要在Storyboard中設定一些UITableViewController與UITableViewCell相關的東西.
- 首先設定RootViewController的Custom Class設定為MasterViewController:
- 其次設定UITableViewCell的Identify為FruitCell,就和我上們面的程式碼一致,然後再設定Style為Basic:
現在我們可以執行一下,這個時候你會發現總共有十行,每行的標題都是一樣的,而且每當我們點選任何的一行都不會發生任何事情.
這是因為我們還沒有建立DetailViewController,現在我們來建立對應的DetailViewController,和上面一樣,這個過程就忽略掉,只是名稱為DetailViewController.
建立完成之後,我們按照同樣的方式,在Main.storyboard中,給DetailViewController設定Custom Class為DetailViewController.
為了讓我們的DetailViewController能夠有東西顯示,這裡加一個UILabel然後,寫上全世界程式設計師第一句學的程式碼Hello Wirkd!,然後在iPad模擬器上執行:
現在每當我們點選一下UITableViewCell,在DetailViewController中就會顯示一個Hello World!.
製作資料模型
基礎知識講完了,接下來是來製作我們需要顯示的資料模型,由於現在我們是在演示,所以這裡的資料模型不會很複雜,更加不會使用到資料持久化.
首先我們需要建立一個名為Fruit類,這裡使用的模板是:
我們建立的這個類,其中包含了水果的圖片,名稱,介紹等等:
import UIKit
struct Fruit {
let name: String
let description: String
let iconName: String
init(name: String,description: String,iconName: String) {
self.name = name
self.description = description
self.iconName = iconName
}
var icon: UIImage? {
return UIImage(named: iconName)
}
}
複製程式碼
現在回到我們的MasterViewController.
顯示水果列表
開啟MasterViewController.swift之後,我們需要新增一個let的屬性:
let fruits = [
Fruit(name: "Apple",description: "這是蘋果",iconName: "Apple"),Fruit(name: "Banana",description: "這是香蕉",iconName: "Banana"),Fruit(name: "Blackberry",description: "這是黑莓",iconName: "Blackberry"),Fruit(name: "Cherries",description: "這是櫻桃",iconName: "Cherries"),Fruit(name: "Coconut",description: "這是椰子",iconName: "Coconut"),Fruit(name: "Grapes",description: "這是葡萄",iconName: "Grapes")
]
複製程式碼
這是我們用來顯示的具體資料陣列.
然後我們找到tableView(_:numberOfRowsInSection:)
並將return語句替換為以下內容:
return fruits.count
複製程式碼
接下來,我們需要將名稱顯示到UITableViewCell中,找到tableView(_:cellForRowAtIndexPath:)
,並且在return的語句之前新增下面的程式碼:
let fruti = fruits[indexPath.row]
cell.textLabel?.text = fruti.name
複製程式碼
這將會把水果的名稱顯示到UITableViewCell中,現在我們執行一下專案:
現在我們成功的將水果名稱顯示在UITableViewCell中了.
更改MasterViewController的標題
為了讓MasterViewController的標題看起來更加的貼合,我們可以修改一下它的標題,修改標題有兩種方式,第一種是直接在Storyboard中修改:
第二種是在程式碼上修改:
override func viewDidLoad() {
super.viewDidLoad()
title = "Fruit List"
}
複製程式碼
無論哪種都可以,但如果你所在的公司有固定的程式碼規範,那就按照公司的規範來.
顯示水果的詳細內容
現在在TableView中我們已經顯示了水果的名稱,現在我們是時候來完善每當點選Cell的時候,DetailViewController則會顯示對應的內容了.
開啟Main.storyboard,將原來我們新增到DetailViewController裡面的內容刪掉,然後再新增我們所需要展示的內容:
下面是我們需要新增的內容:
- 最左邊是用來顯示水果樣子的UIImageView,它的尺寸是128x128
- 右邊最上面的是用來顯示水果名稱的UILabel,它用的是系統粗體,字號為28
- 右邊最下面的是用來顯示水果詳情的UILabel,它用的是系統常規,字號為17
- 這裡使用了兩個UIStackView,第一個是用在兩個UILabel裡,排序方式是豎向的,並且設定它們之間的間距為10,第二個則是用在UIImageView和第一個UIStackView,它的排序方式是橫向的,並且設定它們之間的間距為15.
這裡使用UIStackView可以幫助我們省下很多使用AutoLayout的佈局問題,現在佈局完成了,我們來將UIKit控制元件和DetailViewController關聯.
開啟DetailViewController.swift,然後將下面的程式碼新增進去:
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
var furti: Fruit? {
didSet {
refreshUI()
}
}
private func refreshUI() {
loadViewIfNeeded()
nameLabel.text = furti?.name
descriptionLabel.text = furti?.description
imageView.image = furti?.icon
}
複製程式碼
接下來,我們則需要在Storyboard中,去關聯這裡的屬性:
現在,我們準備就緒了,只差將資料顯示就可以了,開啟SceneDelegate.swift,然後將下面的程式碼替換scene(_:willConnectTo:options :)
的內部實現:
guard
let splitViewController = window?.rootViewController as? UISplitViewController,let leftNavController = splitViewController.viewControllers.first as? UINavigationController,let masterViewController = leftNavController.viewControllers.first as? MasterViewController,let detailViewController = (splitViewController.viewControllers.last as? UINavigationController)?.topViewController as? DetailViewController
else { fatalError() }
let firstMonster = masterViewController.fruits.first
detailViewController.furti = firstMonster
複製程式碼
在UISplitViewController中,它有一個名為ViewControllers的屬性,其中是包含了MasterViewController和DetailViewController,在我們這個情況下,MasterViewController實際上就是NavigationController,所以我們如果要獲取真正的MasterViewController例項,就需要獲得NavigationController中的一個ViewController.
要獲取DetailViewController也是使用同樣的方式,只不過是獲取UISplitViewController中的ViewControllers最後一個ViewController.
現在我們執行專案,就可以看到有關於水果的詳情資訊:
但現在我們又面臨了一個問題,無論我們點選哪個UITableViewCell,都只會顯示蘋果的資訊,接下來我們就需要解決這個問題.
使用delegate完善DetailViewController的顯示內容
關於兩個控制器之間的通訊方式有很多種,在Master-Detail App的模板中,MasterViewController有著對DetailViewController的引用,這就意味著MasterViewController可以在DetailViewController上設定屬性了.
這如果只是在一個簡單的ViewController中則可以直接使用,但在我們這種情況,還是遵循UISplitViewController類引用中的建議方法來處理,那就是新增delegate.
開啟MasterViewController.swift,並且在這個類的上面新增下面的程式碼:
protocol FruitSelectionDelegate: class {
func furitSelected(_ newFurit: Fruit)
}
複製程式碼
這定義了一個帶有名為furitSelected方法的協議,我們將會在DetailViewController中實現這個方法,並且在MasterViewController中將使用者選擇的水果傳送過去.
接下來,我們需要定義一個delegate屬性:
weak var delegate: FruitSelectionDelegate?
複製程式碼
這就意味著delegate屬性需要一個實現了furitSelected(_:)
方法的物件,由於我們希望DetailViewController在使用者點了不同的水果時就更新內容,所以我們需要在DetailViewController中實現這個代理方法:
extension DetailViewController: FruitSelectionDelegate {
func furitSelected(_ newFurit: Fruit) {
self.furti = newFurit
}
}
複製程式碼
這裡我們使用extension可以讓程式碼看起來更加的明確,現在我們在DetailViewController中實現了這個代理方法,那麼接下來,我們還需要在MasterViewController裡,實現這個傳遞的細節:
override func tableView(_ tableView: UITableView,didSelectRowAt indexPath: IndexPath) {
let selectedFruits = fruits[indexPath.row]
delegate?.furitSelected(selectedFruits)
}
複製程式碼
最後,我們還需要在SceneDelegate.swift的scene(_:willConnectTo:options:)
方法中,將DetailViewController設定為MasterViewController的代理:
masterViewController.delegate = detailViewController
複製程式碼
現在我們已經完成了MasterViewController和DetailViewController之間的通訊了.
現在看上去一切都好像非常的完美,但如果我們需要在iPhone上執行它,那麼在MasterViewController選擇水果時就不會顯示DetailViewController了,這裡我們需要進行一丟丟的優化,確保在iPhone上也可以正常執行.
開啟MasterViewController.swift,找到tableView(_:didSelectRowAt:)
方法,然後將下面的內容新增到內部程式碼的最後面:
if let detailViewController = delegate as? DetailViewController {
splitViewController?.showDetailViewController(detailNavigationController,sender: nil)
}
複製程式碼
首先,我們確保我們的Delegate物件是DetailViewController例項,然後在UISplitViewController上呼叫showDetailViewController(_: sender:)
時,將DetailViewController傳遞進去,這裡有一點要說明,UIViewController本身是有一個叫做splitViewController的屬性,它將會引用已經存在的ViewController.
經過這個簡單的改動,在iPhone上它就可以正常運行了,只是添加了幾行程式碼,我們就可以在iPad和iPhone上使用完整的UISplitViewController了.
完善iPad的縱向顯示
在橫向顯示的時候,iPad會自動在左邊顯示選單欄,但是在縱向時,只能通過手勢從左往右的滑動才會顯示,在點選選單欄以外的位置,它就會自動隱藏掉.
雖然這種滑動顯示的方式很高大上,但如果我們要像iPhone那樣,在左上方有一個顯示選單的按鈕該怎麼做呢? 這個時候我們只需要對App進行一丟丟的優化就可以了.
首先我們開啟Main.storyboard,給DetailViewController新增一個UINavigationController,這裡有兩種新增的方式.
- 第一種
- 第二種
無論哪種其實都是可以的,沒有任何的區別,下面是完成了新增的storyboard:
現在我們開啟MasterViewController,找到tableView(_:didSelectRowAt:)
,修改一下我們之前呼叫showDetailViewController(_:sender:)
的小細節:
if let detailViewController = delegate as? DetailViewController,let detailNavigationController = detailViewController.navigationController {
splitViewController?.showDetailViewController(detailNavigationController,sender: nil)
}
複製程式碼
這裡我們將顯示DetailViewController修改成顯示DetailViewController的NavigationController,但無論怎麼改,這個NavigationController的根檢視依然是DetailViewController,和我們之前看到的內容依然是一樣的.
接下來,我們需要在SceneDelegate.swift的scene(_willConnectTo:options:)
中,修改初始化detailViewController的程式碼:
let detailViewController = (splitViewController.viewControllers.last as? UINavigationController)?.topViewController as? DetailViewController
複製程式碼
因為DetailViewController是存在於UINavigationController中,所以我們這裡需要通過訪問兩層來獲取到它的例項.
最後,我們在方法結束之前,新增下面的兩行程式碼:
detailViewController.navigationItem.leftItemsSupplementBackButton = true
detailViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
複製程式碼
這兩行程式碼是用來告訴DetailViewController的左上角按鈕是用來顯示UISplitViewController的,在iPhone不會有任何的改變,但是在iPad上會有一個按鈕用來顯示選單的UITableView.
下面就是我們執行的效果:
總結
通過簡單的事例,我們學習了UISplitViewController的使用,雖然這個例子比較簡單,但是可以通過該例子慢慢的延展出更多的使用場景,謝謝大家的閱讀.