UITabBarController 結(jié)合 UINavigationController厢岂、UITableViewController湘今,在 iOS App 的 UI 設(shè)計(jì)中是比較經(jīng)典的組合用法,效果可以參考原生電話 App塘娶。
本文我們要實(shí)現(xiàn)的是栓票,在點(diǎn)擊導(dǎo)航欄的按鈕后密任,隱藏 TabBar颜启,顯示自定義的工具欄菜單,再次點(diǎn)擊按鈕切換回來浪讳。
本文的 示例工程 已上傳至 Github缰盏,歡迎下載調(diào)試,完成后 App 顯示效果如下:
下面我們從頭開始創(chuàng)建示例工程:
1.首先編程環(huán)境使用 Xcode 9.1
版本淹遵、Swift 4.0
語言口猜,支持 iOS 10
,新建工程 Single View App
-> 工程名: SwitchBetweenCustomToolBarAndTabBar
透揣。
2.新建文件济炎,選擇 UITableViewController
模版,命名為 MainTableViewController
辐真。
3.再新建一個(gè) UITableViewController
模版须尚,命名為 DetailTableViewController
。
4.在 Main.storyboard
中侍咱,刪除默認(rèn)視圖耐床,拖拽兩個(gè) Table View Controller
,分別關(guān)聯(lián)至剛才創(chuàng)建的 MainTableViewController
楔脯、DetailTableViewController
撩轰。
5.選擇 MainTableViewController
,在菜單欄選擇 Editor
-> Embed in
-> Navigation Controller
,效果如下:
6.在 StoryBoard
上對兩個(gè)表格做些基本設(shè)置昧廷,第一個(gè)表格設(shè)置為 Dynamic Prototypes
钧敞、Grouped
、1 Rows
麸粮。選中 Cell,選擇 style Right Detail
镜廉,設(shè)置 Identifier
為 bookCell
弄诲,Accessory 選擇 Disclosure Indicator
。選中視圖上的 Navigation Item
娇唯,設(shè)置標(biāo)題為:“書籍列表”齐遵。再拖入一個(gè) Bar Button Item
放在導(dǎo)航欄右側(cè),命名為:“編輯”塔插。
7.第二個(gè)表格設(shè)置為 Static Cells
梗摇、 Grouped
、 1 Sections
想许、 3 Rows
伶授,再拖拽一個(gè) Navigation Item
断序,并設(shè)置標(biāo)題為:“書籍詳情”。選中 Cell糜烹,選擇 style Right Detail
违诗,修改標(biāo)簽名稱。在兩個(gè)表格之間創(chuàng)建一個(gè) Selection Segue
疮蹦,選中第一個(gè)表格的 Cell诸迟,按住 control
連接至第二個(gè)表格,在 Selection Segue
下選擇 Show
愕乎,選擇 Segue
阵苇,設(shè)置 Identifier
為 showDetail
。效果如下:
8.在 StoryBoard
中再拖入一個(gè) Tab Bar Controller
感论,默認(rèn)自帶兩個(gè)標(biāo)簽頁绅项,選中 Tab Bar Controller
,按住 control
連接至 Navigation Controller
笛粘,選擇 Relationship Segue
下的 view controllers
趁怔。選中 Tab Bar
中的標(biāo)簽圖標(biāo),移動(dòng)下標(biāo)簽順序薪前,拖動(dòng)即可润努,將我們要展示的表格放在前面。選中 Navigation Controller
中的 Item
示括,修改 title
為“書籍列表”铺浇。設(shè)置下每個(gè)標(biāo)簽的圖標(biāo)。最后選中 Tab Bar Controller
垛膝,勾選 Is Initial View Controller
△⒙拢現(xiàn)在效果如下:
9.現(xiàn)在來做點(diǎn)代碼工作。打開 MainTableViewController
吼拥,添加編輯按鈕的 IBOutlet
倚聚、添加初始數(shù)據(jù)、完善數(shù)據(jù)源方法等凿可,代碼如下:
// MARK: 1.--@IBOutlet屬性定義-----------??
@IBOutlet weak var editButton: UIBarButtonItem!
// MARK: 2.--實(shí)例屬性定義----------------??
var bookList = [
["name": "讀庫","author": "張立憲", "press": "新星出版社"],
["name": "三體","author": "劉慈欣", "press": "重慶出版社"],
["name": "驅(qū)魔","author": "韓松", "press": "上海文藝出版社"],
["name": "葉曼拈花","author": "葉曼", "press": "中央編譯出版社"],
["name": "南華錄 : 晚明南方士人生活史","author": "趙柏田", "press": "北京大學(xué)出版社"],
["name": "青鳥故事集","author": "李敬澤", "press": "譯林出版社"],
["name": "可愛的文化人","author": "俞曉群", "press": "岳麓書社"],
["name": "呼吸 : 音樂就在我們的身體里","author": "楊照", "press": "廣西師范大學(xué)出版社"],
["name": "書生活","author": "馬慧元", "press": "中華書局"],
["name": "葉彌六短篇","author": "葉彌", "press": "海豚出版社"],
["name": "美哉少年","author": "葉彌", "press": "江蘇鳳凰文藝出版社"],
["name": "新與舊","author": "沈從文", "press": "重慶大學(xué)出版社"],
["name": "銀河帝國:基地","author": "艾薩克·阿西莫夫", "press": "江蘇文藝出版社"],
["name": "世界上的另一個(gè)你","author": "朗·霍爾 丹佛·摩爾", "press": "湖南文藝出版社"],
["name": "奇島","author": "林語堂", "press": "群言出版社"]
]
// MARK: 3.--視圖生命周期----------------??
override func viewDidLoad() {
super.viewDidLoad()
tableView.allowsMultipleSelectionDuringEditing = true // 允許編輯模式下多選
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: 4.--處理主邏輯-----------------??
/// 切換表格的編輯與瀏覽狀態(tài)
func switchEditMode() {
if tableView.isEditing {
self.setEditing(false, animated: true) // 結(jié)束編輯模式
editButton.title = "編輯"
} else {
self.setEditing(true, animated: true) // 進(jìn)入編輯模式
editButton.title = "取消"
}
}
// MARK: 5.--輔助函數(shù)-------------------??
// MARK: 6.--動(dòng)作響應(yīng)-------------------??
@IBAction func editButtonTapped(_ sender: Any) {
switchEditMode()
}
// MARK: 7.--事件響應(yīng)-------------------??
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
}
override func shouldPerformSegue(withIdentifier identifier: String,
sender: Any?) -> Bool {
// 編輯模式下禁止觸發(fā) segue
if tableView.isEditing {
return false
} else {
return true
}
}
// MARK: 8.--數(shù)據(jù)源方法------------------??
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return bookList.count
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell
{
let cell = tableView.dequeueReusableCell(
withIdentifier: "bookCell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = bookList[row]["name"]
cell.detailTextLabel?.text = bookList[row]["author"]
return cell
}
10.現(xiàn)在就可以看到瀏覽狀態(tài)和編輯多選狀態(tài)兩種效果:
11.再完善一下書籍詳情頁惑折,打開 DetailTableViewController
,添加代碼如下:
// MARK: 1.--@IBOutlet屬性定義-----------??
// MARK: 2.--實(shí)例屬性定義----------------??
var bookDetail = ["name": "","author": "", "press": ""]
// MARK: 3.--視圖生命周期----------------??
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: 4.--處理主邏輯-----------------??
// MARK: 5.--輔助函數(shù)-------------------??
// MARK: 6.--動(dòng)作響應(yīng)-------------------??
// MARK: 7.--事件響應(yīng)-------------------??
// MARK: 8.--數(shù)據(jù)源方法------------------??
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell
{
let cell = super.tableView(tableView, cellForRowAt: indexPath)
let row = indexPath.row
switch row {
case 0:
cell.detailTextLabel?.text = bookDetail["name"]
case 1:
cell.detailTextLabel?.text = bookDetail["author"]
case 2:
cell.detailTextLabel?.text = bookDetail["press"]
default:
break
}
return cell
}
// MARK: 9.--視圖代理方法----------------??
12.回到 MainTableViewController
枯跑,補(bǔ)充一下 prepare
方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let detailVC = segue.destination as! DetailTableViewController
let cell = sender as! UITableViewCell
let selectedIndexPath = tableView.indexPath(for: cell)!
detailVC.bookDetail = bookList[selectedIndexPath.row]
}
13.現(xiàn)在詳情頁也能看到內(nèi)容了:
14.接下來我們希望在編輯書籍列表的時(shí)候惨驶,頁面底部顯示工具欄,方便我們進(jìn)行刪除等操作敛助。Navigation Controller
是自帶 Toolbar 的粗卜,只需要選中它并勾選 Shows Toolbar
就行,但是默認(rèn)效果實(shí)在是不友好纳击,Toolbar 和 Tabbar 緊挨著续扔,既占空間又不美觀攻臀,這也是我要寫這篇文章的主要原因:
15.因此我們接下來要嘗試,在編輯時(shí)隱藏 Tabbar测砂,只顯示 ToolBar茵烈,在結(jié)束編輯時(shí),隱藏 ToolBar砌些,重新顯示 Tabbar呜投。其實(shí),另一個(gè)系統(tǒng)自帶的 App 已經(jīng)實(shí)現(xiàn)了這個(gè)效果存璃,就是照片 App仑荐,效果看下圖。但是仔細(xì)看它也有一個(gè)問題纵东,它在點(diǎn)擊“選擇”按鈕時(shí)粘招,顯示的工具欄是 UIToolbar
類型的,它的高度比 Tabbar 要矮一點(diǎn)偎球,這樣在切換時(shí)感覺不協(xié)調(diào)(除了這個(gè)問題洒扎,iOS 11 上的照片 App 還有其他問題)。
16.所以我們打算自定義 Toolbar衰絮,且要滿足以下幾個(gè)特性:
- 高度和 Tabbar 一致
- 顏色一致
- 上邊沿要有根橫線
- 帶背景毛玻璃效果
是不是覺得我們要復(fù)刻 Tabbar 了袍冷?看起來還真有點(diǎn)像,不過我們會(huì)做的稍微簡單點(diǎn)猫牡,看完本文還有想法的可以再去打磨一下胡诗。
問題是我們怎么能做的這么像呢?這要多謝 Xcode 的 View Debugging
功能淌友,可以把 Tabbar 刨開來看個(gè)夠煌恢。
17.接下來打開 Xcode,運(yùn)行一下工程震庭,打開菜單欄:Debug
-> View Debugging
-> Capture View Hierarchy
瑰抵,可以把 App 視圖層次屬性看的清清楚楚:
18.下面我們來逐個(gè)實(shí)現(xiàn) Toolbar 需要的特性,先從毛玻璃效果開始器联。
- 打開 Xcode 新建
UIVIew
的子類ToolBarView.swift
谍憔,再創(chuàng)建一個(gè) View 的 xib 文件ToolBarView.xib
。 - 在 xib 文件中選中 View 主籍,將
Custom Class
設(shè)置為ToolBarView
(這里不在 File's Owner 里設(shè)置,很多問答的回復(fù)里亂用 File's Owner 逛球,下次專題講解自定義 UIView 的問題)千元,在Simulated Metrics
的 Size 項(xiàng)中選擇Freeform
,再在尺寸設(shè)置中將 View 高度改為 49颤绕。 - 從 UI 模版庫中找到
Visual Effect Views With Blur
幸海,拖入 View 中祟身,設(shè)置約束和 View 保持相同高寬、左上對齊物独。選中Visual Effect View
袜硫,找到設(shè)置項(xiàng)Blur Style
,選擇Extra Light
挡篓。
19.經(jīng)過前面的觀察婉陷,Tabbar 上邊沿的細(xì)橫線,其實(shí)是一個(gè)高度為 0.33官研、帶有背景色的空 Image View秽澳,用法是不是很特別。接著我們在 UI 庫中找到 Image View
戏羽,放在 Visual Effect View
上層担神,并設(shè)置約束,高度的約束單獨(dú)設(shè)置為0.33(高度直接在 View 尺寸中設(shè)置是不起作用的)始花,其他約束相同妄讯。找到 Image View
的 Background
屬性,設(shè)置為黑色加 30% 透明度酷宵。
20.再拖拽一個(gè) Button 到 ToolBarView
上亥贸,將約束設(shè)置為上下左右居中即可,標(biāo)題設(shè)置為:“刪除”忧吟。到這一步為止砌函,你應(yīng)該在 xib 上看到以下層次結(jié)構(gòu):
21.打開 ToolBarView.swift
,在 Xcode 中創(chuàng)建一個(gè) IBOutlet 關(guān)聯(lián)至“刪除”按鈕溜族,并添加以下代碼:
class ToolBarView: UIView {
@IBOutlet weak var deleteButton: UIButton!
class func initView() -> ToolBarView {
let myClassNib = UINib(nibName: "ToolBarView", bundle: nil)
let toolBarView = myClassNib.instantiate(
withOwner: nil,
options: nil)[0] as! ToolBarView
return toolBarView
}
}
22.打開 MainTableViewController.swift
讹俊,添實(shí)例屬性:
/// 工具欄視圖
var toolBarView: ToolBarView?
/// 編輯狀態(tài)下選中的書籍?dāng)?shù)組
var selectedBooksIndexs: [Int] {
guard let indexPaths = tableView.indexPathsForSelectedRows else {
return []
}
var indexs: [Int] = []
for indexPath in indexPaths {
indexs.append(indexPath.row)
}
return indexs
}
修改 viewDidLoad()
方法如下:
override func viewDidLoad() {
super.viewDidLoad()
tableView.allowsMultipleSelectionDuringEditing = true // 允許編輯模式下多選
initialToolBar() // 初始化工具欄
}
添加方法 initialToolBar()
:
/// 初始化工具欄
func initialToolBar() {
toolBarView = ToolBarView.initView() // 初始化工具欄對象
setupToolBarFrame() // 對工具欄進(jìn)行布局
// 添加至 TabBar 視圖中
self.tabBarController?.view.addSubview(toolBarView!)
toolBarView?.isHidden = true // 默認(rèn)隱藏
registerToolBarButtonAction() // 注冊按鈕點(diǎn)擊事件
}
添加方法 setupToolBarFrame()
:
/// 對工具欄進(jìn)行布局
func setupToolBarFrame() {
var frame = CGRect()
// 工具欄布局與 Tabbar 保持一致
frame.origin = (self.tabBarController?.tabBar.frame.origin)!
frame.size = (self.tabBarController?.tabBar.frame.size)!
toolBarView?.frame = frame
}
添加方法 registerToolBarButtonAction()
:
/// 注冊工具欄按鈕點(diǎn)擊事件
func registerToolBarButtonAction() {
// 刪除按鈕
toolBarView?.deleteButton.addTarget(
self, action: #selector(self.deleteToolBarButtonTapped(_:)),
for: .touchUpInside)
}
添加方法 deleteToolBarButtonTapped(:)
:
/// 響應(yīng)工具欄刪除按鈕點(diǎn)擊
@objc func deleteToolBarButtonTapped(_ sender: UIButton) {
deleteSelectedBooks() // 刪除選擇的書籍
}
添加方法 deleteSelectedBooks()
:
/// 刪除選擇的書籍
func deleteSelectedBooks() {
let indexs = selectedBooksIndexs.sorted()
for index in Array(indexs.reversed()) {
bookList.remove(at: index)
}
tableView.beginUpdates()
tableView.deleteRows(at: indexs.map { IndexPath(row: $0, section: 0) } ,
with: .fade)
tableView.endUpdates()
switchEditMode()
}
完善方法 switchEditMode()
:
/// 切換表格的編輯與瀏覽狀態(tài)
func switchEditMode() {
if tableView.isEditing {
self.setEditing(false, animated: true) // 結(jié)束編輯模式
editButton.title = "編輯"
} else {
self.setEditing(true, animated: true) // 進(jìn)入編輯模式
editButton.title = "取消"
}
switchToolBarAndTabbar() // 切換顯示工具欄
}
添加方法 switchToolBarAndTabbar()
:
/// 切換顯示工具欄
func switchToolBarAndTabbar() {
if tableView.isEditing {
self.tabBarController?.tabBar.isHidden = true // 隱藏 Tab 欄
toolBarView?.isHidden = false // 顯示工具欄
} else {
self.tabBarController?.tabBar.isHidden = false // 顯示 Tab 欄
toolBarView?.isHidden = true // 隱藏工具欄
}
}
23.在 Xcode 中運(yùn)行一下工程,現(xiàn)在就可以愉快地展示自定義 Toolbar 和刪除操作了:
24.最后再解決一個(gè)小問題煌抒,設(shè)備旋轉(zhuǎn)時(shí)需要對工具欄進(jìn)行重新布局仍劈,修改 viewDidLoad()
方法:
override func viewDidLoad() {
super.viewDidLoad()
tableView.allowsMultipleSelectionDuringEditing = true // 允許編輯模式下多選
initialToolBar() // 初始化工具欄
addObserver() // 注冊需要監(jiān)聽的對象
}
添加方法 addObserver()
:
/// 注冊需要監(jiān)聽的事件
func addObserver() {
// 監(jiān)聽設(shè)備旋轉(zhuǎn)事件
NotificationCenter.default.addObserver(
self,
selector: #selector(self.updateLayoutWhenOrientationChanged),
name: NSNotification.Name.UIDeviceOrientationDidChange,
object: nil)
}
添加方法 updateLayoutWhenOrientationChanged()
:
/// 設(shè)備旋轉(zhuǎn)時(shí)重新布局
@objc func updateLayoutWhenOrientationChanged() {
setupToolBarFrame() // 對工具欄進(jìn)行布局
}
25.現(xiàn)在再看是不是很棒!我們這篇教程到這里就結(jié)束了寡壮,謝謝大家的耐心閱讀贩疙!
后記:寫這篇教程花了三天時(shí)間,我沒有預(yù)計(jì)到居然這么漫長况既。其實(shí)當(dāng)時(shí)解決問題寫代碼的時(shí)間很快这溅,只要一個(gè)小時(shí)左右。寫教程不像調(diào)試代碼棒仍,它需要在邏輯上一氣呵成悲靴,因此前后不斷的更換截圖、更新代碼莫其,而且示例雖然簡單癞尚,但要盡量做到合理封裝耸三、邏輯清晰,在代碼規(guī)范上也是一個(gè)必要的示范浇揩。我還會(huì)繼續(xù)堅(jiān)持寫教程仪壮,相信以后會(huì)越寫越快、越寫越清晰胳徽。大家有什么建議隨時(shí)提啊积锅,我的郵箱: pmtnmd@163.com 。
歡迎訪問 我的個(gè)人網(wǎng)站 膜廊,閱讀更多文章乏沸。
題圖:The road to Mt Cook - Quentin Leclercq @unsplash
“你的喜愛就是我的動(dòng)力,歡迎各位打賞”