一年半开发经验,使用 RxSwift 构建一个项目的基本框架,这种姿势足够优雅吗?

如果你跟我一样,工作了一年半,相信你也经历过几个大版本的迭代开发,甚至拥有几个应用的开发经验。那么在实现过那么多复杂的业务逻辑之后,你觉得,利用纯代码,怎么样去创建一个美、优、简以及便于管理的基本框架呢? 这两天在思考这个问题,于是有了以下想法(可能还不是很成熟,如果你有更好的想法,请评论告知我,在此提前感谢)。

撸撸思路

今天以搭建一个项目的基本框架为例,来谈谈这个问题,最终的效果图如下:

显示效果

大家可以看到,这确实是一个复杂项目的基础,即使逻辑再复杂的界面,最开始也都是从这个界面起步的,而对于它,我们需要做的:

  • 初始化几个子控制器
  • 添加自定义的 TabBarView
  • 添加自定义的 NavigationBar
  • 添加自定的 TopBarColumnView

准备工作

这里主要是准备一些项目开发需要的资源和添加一些需要使用到的第三方库,过程简单、琐碎:

  • 导入需要使用到的全部图片资源
  • 导入需要使用到的第三方: RxSwift、RxCocoa
  • 结合 UIStoryBoard 初始化几个控制器,并在每个控制器上添加初始化显示图片

完成上述操作之后,可得项目结构如下:

目录名称 功能介绍
Main 项目的主要入口
Main - MenuViewController 菜单控制器,作用与 TabBarController 相同
Business 存储所有的业务逻辑模块
Business - Home 首页业务模块
Business - Medium 书影音 业务模块
Business - Broadcast 广播 业务模块
Business - Group 小组 业务模块
Business - Profile 我的 业务模块
Utility 存储所有的功能模块
Utility - Extension 存储所有功能类的功能扩展

当然,大家觉得无聊就直接跳过这个操作,在 github 上 https://github.com/iJudson/RxBasicInterface.git <前期准备> 该 commit 上直接拿到这一步的代码

正题

结束了所有的准备工作,接下来进入正题:使用 RxSwift 创建 TabBarView 供菜单控制器 MenuViewController 使用

TabBarView 的创建

其实对于某些 View 的使用,即使是自己创建的,往往会在过了一段较长的时间之后,再去使用这张 View 时,竟然发现无从下手,需要再重温下代码,特别是对于那些特别复杂的 View,有多个便利构造函数,或者特别多的子控件,而且需要根据不同情况去使用其中的某些控件...
还有一种情况, 你在公司与其他人协同工作时,会不会发现要去使用到他创建的较为复杂的 View 时,需要或一些时间,感到特别烦呢?
针对这两个问题,我的解决方案是:

<创建该 View 专属的样式类>
在这里,你可以先去跟你的同事或跟自己约定一套创建 View 的风格,就是在创建该 View 的时候先去创建该 View 的专属样式类:用于管理该 View 的所有样式:颜色、子控件类型和数量、子控件的显示位置...
我们想创建 TabBarView,让我们先来创建 TabBarView 的样式管理类:

typealias TabBarItemElement = (normalImage: UIImage?, selectedImage: UIImage?, title: String?)

// TabBarView 的样式管理类
struct TabBarStyle {
    //  子控件的元素集合   
    var itemElements: [TabBarItemElement]
     //  TabBarView 的颜色主题(默认为白色)   
    var themeColor: UIColor = UIColor.white
    //  是否含有顶部分割线   
    var hasTopLine: Bool = true
    // 在初始化构造器中,只去初始化必要的元素,避免初始化构造器过于冗长
    init(elements: [TabBarItemElement] = []) {
        self.itemElements = elements
    }
}

<创建 TabBarView>
有了上面的样式管理类,我们在该 View 中,对外的属性就特别少,基本就只有两个,这也大程度的增强了该 View 的封装性。
1. 定义对外的样式属性:

// 主题样式
var themeStyle: TabBarStyle = TabBarStyle() {
    didSet {
        // 通过属性监视器,外部可以通过该属性更新 TabBarView 的整体样式
        update(ThemeStyle: themeStyle)
    }
}

2. 创建便利构造函数:便于外部对该类的使用

convenience init(frame: CGRect? = nil, themeStyle: TabBarStyle) {
    // 当外部不传入 Frame 值时,我们默认使用以下 Frame 值
    let barFrame = frame ?? CGRect(x: 0, y: screenHeight - tabBarHeight, width: screenWidth, height: tabBarHeight)
    self.init(frame: barFrame)
    // 设置背景颜色
    self.backgroundColor = themeStyle.themeColor
    // 根据样式管理类,更新该 view 的主体样式
    update(ThemeStyle: themeStyle)
    // 根据样式管理类,决定是否添加顶部分割线
    add(TopLine: themeStyle.hasTopLine)
}

OK,到这里这个 View 大体上就创建完毕了。是不是很简单,日后是不是也很便于管理?目前来看,这个 View 只有一个对外属性,还有一个 convenience 构造函数,确实很便于我们管理,而对于样式,我们只需要去查看样式管理类,就只有这个 View 的所有的样式...
哈,其实接下来,还是有些细节要继续完善。

3. 实现样式更新的方法
在其中,我们只需要拿到外部传进来的样式属性,去更新样式。
因为这里的 TabBarItem 既有图片,又有标题,所有外部需要将这两个属性都传进来。

fileprivate func update(ThemeStyle style: TabBarStyle) {
    // 所有的样式元素数量
    let itemCount = style.itemElements.count
    let itemWidth = screenWidth/itemCount
    var commonItemFrame = CGRect(x: 0, y: 0, width: itemWidth, height: tabBarHeight)
    // 通过 for 循环,将拿到的所有元素遍历添加到 TabBarView 上
    for (index, element) in style.itemElements.enumerated() {
        commonItemFrame.origin.x = index * itemWidth
        let commonTabBarIem = CommonItemView(frame: commonItemFrame, image: element.normalImage, title: element.title)
        commonTabBarIem.setImage(element.selectedImage, for: .selected)
        commonTabBarIem.alignmentStyle = .upImageDownText(space: 2)
        commonTabBarIem.tag = index
        commonTabBarIem.titleLabel?.font = UIFont.systemFont(ofSize: 12)
        commonTabBarIem.setTitleColor(UIColor.gray, for: .normal)
        commonTabBarIem.setTitleColor(UIColor.black, for: .selected)
        
        self.addSubview(commonTabBarIem)
        // 这里我们添加监听者,监听每一个 Item 的点击操作,当 item 被点击时,我们通过 Driver 序列,发送点击事情到这和序列上
        let selectedIndex = commonTabBarIem.rx.controlEvent(.touchDown).throttle(0.5, scheduler: MainScheduler.instance).flatMapLatest { (_) -> Driver<Int> in
            return Driver<Int>.just(commonTabBarIem.tag)
            }.asDriver(onErrorJustReturn: 0)
        // 对外监听属性的集合 
        selectedIndexes.append(selectedIndex)
        
        // 默认选中第一个 tabBarItem
        commonTabBarIem.isSelected = (index == 0 ? true : false)
    }
}

在上面方法中,大家应该有注意到,我们需要一个对外监听属性的集合,监听用户的点击操作,因为一般 TabBar 有5个 Item,故而这个集合一般都有五个对外监听属性,属性定义如下:

 //当前选中的 index
 var selectedIndexes: [Driver<Int>] = []

4. 添加顶部分割线

 fileprivate func add(TopLine isExisting: Bool) {
    // 事先判断是否需要添加分割线,如果不需要,可直接 return 返回
    guard isExisting else {
        return
    }
   
    let lineHeight: CGFloat = 0.5
    let topLine = UIView(frame: CGRect(x: 0, y: -lineHeight, width: screenWidth, height: lineHeight))
    topLine.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
    self.addSubview(topLine)
 }

到这一步的代码,大家可以在 https://github.com/iJudson/RxBasicInterface <创建 TabBarView> 当前 commit 上获取(注意,现在在这个 commit 上运行程序,还是没有任何效果,毕竟还没有使用到该 TabBarView)

TabBarView 在 MenuViewController 的使用

TabBarView 是在 MenuViewController 去使用的, 在 MenuViewController 中也需要去监听 TabBarView 的 selectedIndexes 数组属性,从而监听到 TabBarView 上的点击操作。而实际上,TabBarView 的使用,由于使用了 RxSwift ,也变得特别简单,也方便管理。
1. 我们在 viewDidLoad 添加两个方法

  • 初始化 MenuViewController 中所有的子控制器,并默认让首页控制器显示在主页上
  • 配置底部 TabBarView ,创建所需样式管理类 TabBarViewStyle,并添加所需元素
 override func viewDidLoad() {
    super.viewDidLoad()
    
    initializeTopViewControllers()
    
    configureTabBar()
 }

接下来,我们只需要定义并完善上述两个方法...

2. 初始化所有的子控制器

 fileprivate func initializeTopViewControllers() {
     // 通过 UIStoryboard 拿到所有的子导航控制器
    let homeNav = UIStoryboard.NavigationController.home
    let mediumNav = UIStoryboard.NavigationController.medium
    let broadcastNav = UIStoryboard.NavigationController.broadcast
    let groupNav = UIStoryboard.NavigationController.group
    let profileNav = UIStoryboard.NavigationController.profile
    // 并预先添加到 topViewControllers 数组中,当点击某一个 TabBarItem 时,根据需要添加具体的控制器
    topViewControllers = [homeNav, mediumNav, broadcastNav, groupNav, profileNav]
    
    // 初始化时,默认添加首页控制器
    self.addChildViewController(homeNav)
    homeNav.view.frame = self.view.bounds
    self.view.addSubview(homeNav.view)
    // 默认显示首页控制器
    self.selectedViewController = homeNav
    // 更新状态栏的状态
    self.setNeedsStatusBarAppearanceUpdate()
 }

3. 配置 TabBarView,监听其中的点击属性

  • 我们拿到所有的图片和标题属性
  • 配置 TabBar 样式管理类
  • 监听TabBarItem 的点击操作(这里由于 RxSwift 会变得特别简单)
  • 处理 TabBarItem 的点击事件
 fileprivate func configureTabBar() {
    // 拿到五个 BarItem 上的元素(图片和标题)
    let barItemElements: [TabBarItemElement] = [
        (normalImage: UIImage(named: "ic_tab_home_gray_32x32_"), selectedImage: UIImage(named: "ic_tab_home_32x32_"), title: "首页"),
        (normalImage: UIImage(named: "ic_tab_subject_gray_32x32_"), selectedImage: UIImage(named: "ic_tab_subject_32x32_"), title: "书影音"),
        (normalImage: UIImage(named: "ic_tab_timeline_gray_32x32_"), selectedImage: UIImage(named: "ic_tab_timeline_32x32_"), title: "广播"),
        (normalImage: UIImage(named: "ic_tab_group_gray_32x32_"), selectedImage: UIImage(named: "ic_tab_group_32x32_"), title: "小组"),
        (normalImage: UIImage(named: "profile_normal_32x32_"), selectedImage: UIImage(named: "profile_active_32x32_"), title: "我的")
    ]
    // 创建管理样式类
    let tabBarStyle = TabBarStyle(elements: barItemElements)
    // 拿到样式类,创建 TabBarView 
    let tabBarView = TabBarView(themeStyle: tabBarStyle)
    self.view.addSubview(tabBarView)
    // 监听 TabBarView 的 selectedIndexes 属性,当有点击操作时,触发处理点击事件的方法
    for selectedIndex in tabBarView.selectedIndexes {
        selectedIndex.drive(onNext: { [weak self] (index) in
            guard let strongSelf = self else {
                return
            }
            // 处理点击事件
            strongSelf.handleTopControllerSelectionEvent(currentIndex: index)
            
        }).disposed(by: disposeBag)
    }
 }

而其点击事件的处理如下:

fileprivate func handleTopControllerSelectionEvent(currentIndex: Int) {
   // 移除上一次选择的控制器
    if let selectedViewController = selectedViewController {
        selectedViewController.willMove(toParentViewController: nil)
        selectedViewController.view.removeFromSuperview()
        selectedViewController.removeFromParentViewController()
        selectedViewController.viewWillDisappear(false)
    }
    // 添加当前选择的控制器,并显示
    let currentSelectedViewController = topViewControllers[currentIndex]
    self.addChildViewController(currentSelectedViewController)
    currentSelectedViewController.view.frame = self.view.bounds
    self.view.addSubview(currentSelectedViewController.view)
    currentSelectedViewController.didMove(toParentViewController: self)
    self.selectedViewController = currentSelectedViewController
    self.setNeedsStatusBarAppearanceUpdate()
    self.selectedIndex = currentIndex
    
    // 置顶 TabBar
    for subView in self.view.subviews {
        if let tabBar = subView as? TabBarView {
            self.view.bringSubview(toFront: tabBar)
        }
    }
}

OK,到这里,我们一个简单的项目框架就已经完成,其效果如下:

BasicInterface.gif

你可以在 https://github.com/iJudson/RxBasicInterface <TabBarView 在 MenuViewController 的使用> 的分支上拿到迄今为止的代码。

后续

当然一个简单的项目框架还不止这些可能会有:

  • 导航栏 - NavigationBar
  • 顶部选择栏 - TopBarColumnView

而这两个 View 的创建跟 TabBarView 的创建是一模一样的,大家都试着去创建体会下。我这里也不再赘余,而最终的效果如下:

Last Version BasicInterface.gif

最终的代码,大家可以在 https://github.com/iJudson/RxBasicInterface 中找到。

如果这个基本框架还有哪些地方可以继续优化,请不吝评论告知我。
如果你觉得这个基本框架还不错,欢迎 star。

感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,614评论 4 59
  • 伤口就是 你以为它愈合了 阴天时就会痛 提醒你受过伤 伤口还在 你笑靥如花 却让别人看到眼角眉梢之间露出的端倪 有...
    哀慕熙荣阅读 108评论 0 1
  • 猴年马月,科普的帖子很多,从6月5日到7月3日的29天里,猴年怎么来,马月怎么说,应有尽有,只是在热潮下,在BB外...
    氓夏阅读 154评论 0 0
  • 01 大概五年前的时候,有天我突然感觉脚掌隐隐作痛,自查后发现长了个玉米粒大的东西,而且越来越痛。去附近医院检查医...
    王玬锦阅读 237评论 0 2