美文网首页
项目基本架构的搭建

项目基本架构的搭建

作者: 落叶刺客 | 来源:发表于2017-05-21 01:22 被阅读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属性,这个函数就会被添加到方法列表中。

    相关文章

      网友评论

          本文标题:项目基本架构的搭建

          本文链接:https://www.haomeiwen.com/subject/ssmuxxtx.html