1. 程式人生 > >專案基本架構的搭建

專案基本架構的搭建

一、啟動圖片的設定

  專案啟動圖片的設定有多種方式,但是通常情況下,都是用LaunchImage來管理的。具體的操作方式比較簡單,但是一定要注意,當你設定LaunchImage作為啟動圖片時,一定不要忘記把Launch Screen File中的文字給刪除,並且在執行程式之前,最好是把之前執行過的程式給刪掉:

1240
設定啟動圖片的細節.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)
}

  我們只是添加了一個子控制器,還有其它子控制器需要新增。但是,我們不能再像上面那樣做了。重複的程式碼太多,需要抽一個方法來專門處理子控制器:

1240
系統自帶新增子控制器的方法.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自帶了兩個子控制器,但是它不是我們想要的,直接把它們給刪除:

1240
UITabBarController.png

  選中TabBarController,把它交給QFMainViewController來管理,然後去AppDelegate中把我們寫的視窗相關的程式碼刪掉,最後再去General中設定Main Interface從Storyboard中啟動:

1240
繫結類.png

  回到Main.storyboard檔案中,往裡面拖4個NavigationController,以及一個用來佔位的ViewController,然後右擊TabBarController,將viewControllers分別拖給這幾個子控制器,具體操作如下圖所示:

1240
佈局子控制器.png

  現在裡面控制器非常多,是不是看起來很亂?不過不要緊,我們可以把它們拆分成單獨的Storyboard檔案。選中其中一個子控制器,然後點選選單欄上面的Editor,之後選擇Refactor to Storyboard。具體操作如下圖所示:

1240
使用Storyboard Reference.png

  點選完Refactor to Storyboard之後會彈出一個對話方塊,給新的Storyboard檔案取一個名字,然後點選儲存就可以了:

1240
儲存新的Storyboard檔案.png

  按照同樣的方式,分別處理其它幾個子控制器,佔位用的ViewController暫時不用管。處理完之後,Main.storyboard檔案中大概就是這個樣子:

1240
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屬性,這個函式就會被新增到方法列表中。