專案基本架構的搭建
一、啟動圖片的設定
專案啟動圖片的設定有多種方式,但是通常情況下,都是用LaunchImage來管理的。具體的操作方式比較簡單,但是一定要注意,當你設定LaunchImage作為啟動圖片時,一定不要忘記把Launch Screen File中的文字給刪除,並且在執行程式之前,最好是把之前執行過的程式給刪掉:
設定啟動圖片的細節.png
二、初始化專案
專案配置完成以後,通常情況下,需要重新劃分結構。在iOS開發中,有多種架構可供選擇,最常見的架構是MVC,它在軟體開發過程中有著廣泛的應用。由於MVC本身不是特別完美,後來又衍生出了MVP和MVVM架構。在這裡,我們按照MVVM架構的思想對專案目錄進行重新劃分。
1、使用純程式碼來搭建專案
來到General裡面,把Main Interface裡面的Main給刪掉,來到AppDelegate中自己建立Window:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // 建立Window並制定它的frame window = UIWindow(frame: UIScreen.main.bounds) // 設定window的rootViewController window?.rootViewController = nil // 顯示window window?.makeKeyAndVisible() return true }
此時如果執行程式,肯定是看不到window的,因為我們把它設定為nil。接下來需要自定義TabBarController。新建一個名為QFMainViewController的類,讓它繼承自UITabBarController,然後來到AppDelegate中,將其設定為視窗的根控制器:
// 設定window的rootViewController
window?.rootViewController = QFMainViewController()
此時執行程式就可以看到視窗,只不過它現在還沒有顏色,看到的只是黑乎乎的一片。接下來要給它新增子控制器。根據實際情況,在各模組下面的Controller資料夾中建立對應的子控制器,然後來到QFMainViewController的viewDidLoad中建立子控制器:
override func viewDidLoad() {
super.viewDidLoad()
// 設定TabBar的顏色(僅僅只是設定QFMainViewController中TabBar的顏色)
tabBar.tintColor = UIColor.init(red: 202 / 255.0, green: 155 / 255.0, blue: 104 / 255.0, alpha: 1)
// 建立子控制器(tabBar按鈕對應的子控制器)
let liveChildVc = QFLiveViewController()
// 設定子控制器的屬性
liveChildVc.title = "直播" // 設定子控制器的標題
liveChildVc.tabBarItem.image = UIImage(named: "live-n_25x19_")
liveChildVc.tabBarItem.selectedImage = UIImage(named: "live-p_25x19_")
// 包裝導航控制器
let liveChildVcNav = UINavigationController(rootViewController: liveChildVc)
// 新增子控制器
addChildViewController(liveChildVcNav)
}
我們只是添加了一個子控制器,還有其它子控制器需要新增。但是,我們不能再像上面那樣做了。重複的程式碼太多,需要抽一個方法來專門處理子控制器:
系統自帶新增子控制器的方法.png
我們看到,系統自帶了一個新增子控制器的方法。但是,它不滿足我們的要求,因為我們要傳的引數遠不止一個。為此,需要自定義新增子控制器的方法:
override func viewDidLoad() {
super.viewDidLoad()
// 建立子控制器(tabBar按鈕對應的子控制器)
addChildViewController(childVc: QFLiveViewController(), title: "首頁", imageName: "live")
addChildViewController(childVc: QFRankViewController(), title: "排行", imageName: "ranking")
addChildViewController(childVc: UIViewController(), title: "", imageName: "") // 佔位用的
addChildViewController(childVc: QFFoundViewController(), title: "發現", imageName: "found")
addChildViewController(childVc: QFMineViewController(), title: "我的", imageName: "mine")
}
// 新增子控制器
fileprivate func addChildViewController(childVc: UIViewController, title: String, imageName: String) {
// 設定子控制器的屬性
childVc.title = title // 設定子控制器的標題
childVc.tabBarItem.image = UIImage(named: imageName + "-n_25x19_") // live-n_25x19_
childVc.tabBarItem.selectedImage = UIImage(named: imageName + "-p_25x19_") // live-p_25x19_
// 包裝導航控制器
let childVcNav = UINavigationController(rootViewController: childVc)
// 新增子控制器
addChildViewController(childVcNav)
}
在OC中,我們不能像上面那樣自定義方法,因為方法名相同,系統在傳送訊息時,不知道將其發給誰。但是,在Swift是可以的。因為Swift支援方法過載。所謂的方法過載,就是指方法名相同,但是引數不同。而引數不同又有兩重含義,即引數的型別不同,以及引數的個數不同。另外,這個方法最好是私有的,其它地方的類應該是不能訪問的,所以我們應該給它加上訪問限制fileprivate。
在Swift中,與訪問許可權有關的關鍵字主要有4個,它們既可以修飾屬性,也可以修飾函式,主要為:
1、internal : 表示內部的
①、預設情況下,所有類、屬性、函式的訪問許可權都是internal;
②、表示在本模組(專案\包\target)中都可以訪問
2、fileprivate : 表示在當前原始檔中可以訪(Swift 3.0之後出來的)
①、只有在當前檔案中可以訪問,而其它檔案中是不能訪問的
3、private : 表示私有的
①、只有在當前類中才可以訪問,其它類中是不能訪問的
4、open : 表示公開的(在Swift 2.x中叫public)
①、可以跨模組進行訪問
還有兩點需要補充,第一個是設定全域性的tintColor。因為每一個子控制器都需要設定tabBar的tintColor,所以我們最好是不要在各個子控制器類中單獨設定,而是應該把它放在AppDelegate中進行設定:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// 設定全域性TabBar的顏色
UITabBar.appearance().tintColor = UIColor.init(red: 202 / 255.0, green: 155 / 255.0, blue: 104 / 255.0, alpha: 1)
// 與window有關的程式碼
return true
}
TabBar正中間的那個item是用來佔位的,以後上面需要新增一個按鈕,所以這個item應該是不能點選的,所以我們這裡先把它給禁用掉:
// 禁用佔位控制器TabBar按鈕的點選
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 遍歷TabBarItem中的items
for i in 0..<tabBar.items!.count {
// 取出item
let item = tabBar.items![i]
// 將下標為2的item給禁用掉
if i == 2 {
item.isEnabled = false
break
}
}
}
現在TabBar正中間的這個item已經不能點選了,後面直接在上面新增一個按鈕,然後再監聽它的點選就可以了。
2、通過字串來初始化專案
在上面搭建TabBar子控制器的過程中,我們傳遞的是子控制器物件,接下來我們要用與子控制器對應的字串來搭建TabBar。
修改我們剛才寫的新增子控制器的程式碼,將子控制器物件引數修改為String型別,其它的不變:
// 新增子控制器
fileprivate func addChildViewController(childVcName: String, title: String, imageName: String) {
// 根據傳進來的控制器字串獲取與之對應的class
// 將AnyClass轉成具體的控制器型別
// 根據具體的控制器型別來建立對應的子控制器
}
修改建立子控制器的程式碼,將子控制器對應的字串作為引數傳遞給新增子控制器的方法addChildViewController(childVcName: , title: , imageName: ):
override func viewDidLoad() {
super.viewDidLoad()
// 建立子控制器(tabBar按鈕對應的子控制器)
addChildViewController(childVcName: "QFLiveViewController", title: "首頁", imageName: "live")
addChildViewController(childVcName: "QFRankViewController", title: "排行", imageName: "ranking")
// 佔位時,這裡不要用UIViewController
addChildViewController(childVcName: "QFLiveViewController", title: "", imageName: "") // 佔位用的
addChildViewController(childVcName: "QFFoundViewController", title: "發現", imageName: "found")
addChildViewController(childVcName: "QFMineViewController", title: "我的", imageName: "mine")
}
一般而言,只要有了與類對應的字串,我們就能用NSClassFromString方法來建立物件。但是,Swift有一個地方比較特殊,需要先拿到專案的名稱空間,然後再用名稱空間拼接與類對應的字串名稱,這樣我們才能建立相應的物件:
// 新增子控制器
fileprivate func addChildViewController(childVcName: String, title: String, imageName: String) {
// 獲取專案的名稱空間
guard let nameSpace = Bundle.main.infoDictionary!["CFBundleExecutable"] as? String else {
// 如果名稱空間獲取失敗,直接返回
return
}
// 根據傳進來的控制器字串獲取與之對應的class(名稱空間.子控制器的類名)
guard let childVcClass = NSClassFromString(nameSpace + "." + childVcName) else {
// 如果childVcClass獲取失敗,直接退出
return
}
// 將獲取到的AnyClass轉成具體的控制器型別
guard let childVcType = childVcClass as? UIViewController.Type else {
// 如果轉型別失敗,則直接返回
return
}
// 建立對應的控制器物件
let childVc = childVcType.init()
// 設定子控制器的屬性
childVc.title = title // 設定子控制器的標題
childVc.tabBarItem.image = UIImage(named: imageName + "-n_25x19_")
childVc.tabBarItem.selectedImage = UIImage(named: imageName + "-p_25x19_")
// 包裝導航控制器
let childVcNav = UINavigationController(rootViewController: childVc)
// 新增子控制器
addChildViewController(childVcNav)
}
有一個細節需要注意,因為中間釋出直播是一個按鈕,並不需要建立與之對應的子控制器類,在採用常規方式搭建時,我們用一個並未建立的UIViewController作為佔位就可以了。但是,在使用子控制器類對應的字串方法搭建TabBar時,不能再用這個實際並未建立的UIViewController作為佔位了,而是要用一個已經建立了的類作為佔位,比如說我們這裡使用了QFLiveViewController這個類。
3、通過Json檔案來初始化專案
其實通過Json檔案來初始化專案跟通過字串來初始化專案本質上一樣的,只不過這個字串不是在建立子控制器的時候傳遞進來的,而是通過一個json檔案來獲取的(比如說來自伺服器的json檔案),它在建立的時候,也是需要現在專案中建立對應的類,然後再動態的載入:
override func viewDidLoad() {
super.viewDidLoad()
// 通過json檔案來初始化專案
setupFromJsonFile()
}
// 通過json檔案來初始化專案
fileprivate func setupFromJsonFile() {
// 獲取json檔案的路徑
guard let jsonPath = Bundle.main.path(forResource: "ViewController.json", ofType: nil) else {
return
}
// 將json檔案轉成NSData
guard let jsonData = NSData(contentsOfFile: jsonPath) else {
return
}
// json序列化(這裡要進行異常處理)
guard let anyOb = try? JSONSerialization.jsonObject(with: jsonData as Data, options: .mutableContainers) else {
return
}
// 將anyOb轉成字典陣列
guard let dictArr = anyOb as? [[String: Any]] else {
return
}
// 遍歷陣列中的字典
for dict in dictArr {
// 獲取子控制器對應的字串名稱
guard let childVcName = dict["childVcName"] as? String else {
continue
} // 從字典中取出來的資料是一個Any可選型別,需要現將其轉換成String可選型別,之後才能傳給自定義子控制器的函式
// 獲取子控制器對應的title
guard let title = dict["title"] as? String else {
continue
}
// 獲取子控制器對應的背景圖片名稱
guard let imageName = dict["imageName"] as? String else {
continue
}
// 拿到對應的字串兒,新增子控制器
addChildViewController(childVcName: childVcName, title: title, imageName: imageName)
}
}
新增子控制器的程式碼不用改,只需要修改獲取字串的方式,然後再將從json檔案中獲取到的字串傳遞給它就可以了。最後補充一點關於異常的知識點。如果在呼叫系統的某一個函式的過程中,該函式後面有一個throws,說明該函式會丟擲異常,此時你需要對異常進行處理。在Swift中提供了三種處理異常的方式:
①、try方式:程式設計師手動捕捉異常,在真實的開發環境中用得很少;
②、try?方式:系統幫我們處理異常。如果該函式產生了異常,則返回nil;
如果沒有異常,則返回對應的物件。也就是說,該方式會返回一個可選型別,
因此我們需要對結果進行安全校驗,這個比較常用;
③、try!方式:直接告訴系統,該函式沒有異常。但是,如果該函式真的產生了異常,
那麼程式會崩潰,類似於強制解包,操作起來非常的危險,一般不建議使用
4、通過Storyboard來初始化專案
以前在開發的時候,使用得比較多的可能是純程式碼,因為如果使用Storyboard,可能會因為介面過多而造成混亂。但是,實際上蘋果幕後做了很多工作來推廣Storyboard。在iOS 9中,蘋果引入了Storyboard Reference這個概念,它允許你從segue中引用其他storyboard中的viewController。這意味中你可以保持不同功能模組化,同時Storyboard的體積變小並易與管理。下面我們就用一下Storyboard Reference。
來到Main.storyboard檔案,將裡面的控制器給刪掉,往裡面拖一個UITabBarController控制器,並且讓它成為預設的控制器(勾選is initial View Controller)。UITabBarController自帶了兩個子控制器,但是它不是我們想要的,直接把它們給刪除:
UITabBarController.png
選中TabBarController,把它交給QFMainViewController來管理,然後去AppDelegate中把我們寫的視窗相關的程式碼刪掉,最後再去General中設定Main Interface從Storyboard中啟動:
繫結類.png
回到Main.storyboard檔案中,往裡面拖4個NavigationController,以及一個用來佔位的ViewController,然後右擊TabBarController,將viewControllers分別拖給這幾個子控制器,具體操作如下圖所示:
佈局子控制器.png
現在裡面控制器非常多,是不是看起來很亂?不過不要緊,我們可以把它們拆分成單獨的Storyboard檔案。選中其中一個子控制器,然後點選選單欄上面的Editor,之後選擇Refactor to Storyboard。具體操作如下圖所示:
使用Storyboard Reference.png
點選完Refactor to Storyboard之後會彈出一個對話方塊,給新的Storyboard檔案取一個名字,然後點選儲存就可以了:
儲存新的Storyboard檔案.png
按照同樣的方式,分別處理其它幾個子控制器,佔位用的ViewController暫時不用管。處理完之後,Main.storyboard檔案中大概就是這個樣子:
Storyboard Reference.png
現在看起來就非常簡潔了,我們可以在不同的子控制器所對應的Storyboard檔案中處理具體的問題。不過,需要說明的是,Storyboard Reference不支援iOS 8.0及其以下的版本。如果你希望支援iOS 8.0,最好是用純程式碼來搭建。
最後是進行一些細節的處理,設定子控制器tabBarItem的圖片和標題。然後再來到QFMainViewController的viewDidLoad方法中,新增中間的釋出按鈕:
// 中間釋出直播按鈕懶載入
fileprivate lazy var homePageBtn : UIButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
// 新增中間的釋出按鈕
setupHomePageBtn()
}
// 新增中間的按鈕
fileprivate func setupHomePageBtn() {
tabBar.addSubview(homePageBtn)
// 設定中間按鈕的圖片
homePageBtn.setImage(UIImage(named: "homepage_btn_play_n_67x55_"), for: .normal)
// 設定按鈕的尺寸
homePageBtn.sizeToFit()
// 設定按鈕的位置(將釋出直播的按鈕新增到TabBar正中間)
homePageBtn.center = CGPoint(x: tabBar.center.x, y: tabBar.bounds.size.height * 0.5)
}
接下來,我們要監聽釋出直播按鈕的點選。但是在此之前,我們先來補充一點便利建構函式的知識。
根據給定的圖片來建立一個按鈕,像這種需求在專案中經常碰到,所以最好是單獨給它抽取一個方法。以前在OC中,這種情況一般是給UIButton抽一個分類。但是,Swift中基本上沒有分類這個概念。不過,我們依然可以給系統的類來增加分類方法。新建一個Swift File檔案,名字可以隨便取,但是最好取一個見名知意的名字。然後匯入UIKit框架,給UIButton寫一個extension擴充套件:
extension UIButton {
/// 類方法,根據給定的圖片建立一個按鈕(不是最好的選擇)
class func createButton(imageName: String, backgroundImageName: String) -> UIButton {
// 建立按鈕
let button = UIButton()
/** 設定按鈕的屬性 */
// 設定按鈕的圖片
button.setImage(UIImage(named: imageName), for: .normal)
button.setImage(UIImage(named: imageName + "highlighted"), for: .highlighted)
// 設定按鈕的背景圖片
button.setBackgroundImage(UIImage(named: backgroundImageName), for: .normal)
button.setBackgroundImage(UIImage(named: backgroundImageName + "_highlighted"), for: .highlighted)
// 設定按鈕的尺寸
button.sizeToFit()
return button
}
}
現在在外面你就可以通過UIButton呼叫類方法來建立按鈕了。但是,這是OC喜歡乾的事兒,它不是真正的Swift。在Swift中,建立物件一般都是使用建構函式,所以我們也應該用建構函式。
在Swift中,要對系統類的建構函式進行擴充,一般是使用便利建構函式。用convenience修飾的建構函式叫做便利建構函式,它一般是寫在extension裡面,並且需要明確呼叫self.init()。下面我們就用便利建構函式來改造上面的程式碼:
extension UIButton {
convenience init(imageName: String, backgroundImageName: String) {
self.init()
/** 設定按鈕的屬性 */
// 設定按鈕的圖片
setImage(UIImage(named: imageName), for: .normal)
setImage(UIImage(named: imageName + "highlighted"), for: .highlighted)
// 設定按鈕的背景圖片
setBackgroundImage(UIImage(named: backgroundImageName), for: .normal)
setBackgroundImage(UIImage(named: backgroundImageName + "_highlighted"), for: .highlighted)
// 設定按鈕的尺寸
sizeToFit()
}
}
現在我們在外面建立按鈕時,可以直接使用按鈕的便利構造函數了,直接將圖片名作為引數傳遞進去,高亮背景圖片因為沒有,所以可以傳空:
// 中間釋出直播按鈕懶載入
fileprivate lazy var homePageBtn : UIButton = UIButton(imageName: "homepage_btn_play_n_67x55_", backgroundImageName: "")
override func viewDidLoad() {
super.viewDidLoad()
// 新增中間的釋出按鈕
setupHomePageBtn()
}
// 新增中間的按鈕
fileprivate func setupHomePageBtn() {
tabBar.addSubview(homePageBtn)
// 設定按鈕的位置(將釋出直播的按鈕新增到TabBar正中間)
homePageBtn.center = CGPoint(x: tabBar.center.x, y: tabBar.bounds.size.height * 0.5)
}
接下來是監聽釋出直播按鈕的點選。來到添加發布直播按鈕的方法中,呼叫addTarget(, action: , for: )方法,然後再給QFMainViewController寫一個extension,專門用來處理事件的監聽:
// 新增中間的按鈕
fileprivate func setupHomePageBtn() {
// 新增homePageBtn的程式碼
// 監聽釋出直播按鈕的點選
homePageBtn.addTarget(self, action: #selector(QFMainViewController.homePageBtnClick), for: .touchUpInside)
}
// MARK: - 事件監聽
extension QFMainViewController {
@objc fileprivate func homePageBtnClick() {
//
print("QFMainViewController.homePageBtnClick")
}
}
釋出直播按鈕監聽的方法應該只屬於QFMainViewController這個類,不應該讓其它類來訪問。但是,一旦添加了fileprivate訪問限制,系統就會報找不到方法(unrecognized selector sent to instance)這個錯誤,解決的辦法是在前面加上@objc屬性。其實,事件監聽本質上是傳送訊息,而傳送訊息是OC的特性。在OC中,傳送訊息的步驟是,先將方法包裝成@SEL,然後再去類中查詢方法列表,根據@SEL找到imp指標(也就是我們這個對應的函式指標),之後就是執行這個函式。如果在Swift中將函式宣告成fileprivate,那麼該函式不會被新增到方法列表中。但是,如果在前面再加上@objc屬性,這個函式就會被新增到方法列表中。