项目基本架构的搭建

2017-05-21  本文已影响50人  落叶刺客

一、启动图片的设置

  项目启动图片的设置有多种方式,但是通常情况下,都是用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属性,这个函数就会被添加到方法列表中。

上一篇下一篇

猜你喜欢

热点阅读