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