当前位置:   article > 正文

iOS实际开发中使用数据驱动页面布局

iOS实际开发中使用数据驱动页面布局

引言

在实际的APP开发中,我们通常会首先根据设计团队提供的视觉设计UI来构建我们的应用页面。这些设计通常是最全面和理想化的状态,因为设计师并不需要考虑用户的实际操作和交互。然而,如果我们仅仅根据这些设计进行硬编码,会在应用上线后发现许多难以处理的问题。

例如,有些功能会根据用户的身份选择性地显示或隐藏,有些功能会根据审核状态展示不同的样式,还有一些功能可能会根据运营活动来展示或撤销。如果我们通过硬编码来实现这些需求,那么在隐藏和显示某个功能时,可能需要修改大量代码来重新布局,这将极大地增加开发和维护的复杂度。

数据驱动页面布局

我们可以采用数据驱动页面布局的方案,让页面中的元素更加灵活可控,同时也使页面功能更易于扩展和维护。

案例

“Me”页是一个非常典型的案例。通常,这个页面功能复杂,元素类型多样。当我们看到这个设计时,脑海中应该已经有了大概的布局方案。接下来,我们将分别使用硬编码和数据驱动布局来实现这个页面,并分析它们之间的区别。

硬编码 - 直观布局

首先我们来分析一下页面结构,由于下面是重复的列表,那么很自然的我们就想到使用UITableView来实现的它,那么页面页面大致可以分为三个部分:

  • 导航栏:绿色区域,这里包括了用户昵称和设置按钮。
  • 列表头:红色区域,这里面包括了用户基本信息,VIP标记,钱包入口。
  • 列表:蓝色区域,这里包括了Me页的所有小功能入口,比如等级,成就,榜单等等。

这么划分看起来合情合理,结构也很清晰,那我们接下来就来实现它,代码如下:

  1. /// 列表
  2. let tableView = UITableView(frame: .zero, style: .plain)
  3. override func viewDidLoad() {
  4. super.viewDidLoad()
  5. addNavigationBar()
  6. addTableView()
  7. addTableHeaderView()
  8. }
  9. // 设置导航栏
  10. func addNavigationBar() {
  11. addCustomNavigationBar()
  12. }
  13. // 设置列表
  14. func addTableView() {
  15. tableView.frame = CGRect(x: 0, y: cs_navigationBarHeight, width: CS_SCREENWIDTH, height: CS_SCREENHIGHT - cs_navigationBarHeight)
  16. tableView.delegate = self
  17. tableView.dataSource = self
  18. tableView.backgroundColor = .white
  19. tableView.separatorStyle = .none
  20. self.view.addSubview(tableView)
  21. }
  22. // 设置列表头
  23. func addTableHeaderView() {
  24. let headerView = PHMeHeaderView()
  25. headerView.frame = CGRect(x: 0, y: 0, width: CS_SCREENWIDTH, height: 450.0)
  26. tableView.tableHeaderView = headerView
  27. }

由于我们的重点在于页面的布局方案,这里面就不展示每个元素的具体实现细节了。

总之我们已经按照设计图高度还原了UI,接下来我们来处理一下点击事件:

  1. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  2. if indexPath.row == 0 {
  3. print("排行榜")
  4. } else if indexPath.row == 1 {
  5. print("个人资料")
  6. } else if indexPath.row == 2 {
  7. print("等级")
  8. } else if indexPath.row == 3 {
  9. print("邀请奖励")
  10. }
  11. .....
  12. }

好万事大吉了,看起来已经可以提测验收了,这时候产品突然告诉你,我们要在加一个“任务”到列表里面的第2个位置,这时候该怎么做呢?

似乎也还好,单就点击事件来说,我们只需要以此往下移动就可以了,修改后代码如下:

  1. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  2. if indexPath.row == 0 {
  3. print("排行榜")
  4. } else if indexPath.row == 1 {
  5. print("任务")
  6. } else if indexPath.row == 2 {
  7. print("个人资料")
  8. } else if indexPath.row == 3 {
  9. print("等级")
  10. } else if indexPath.row == 4 {
  11. print("邀请奖励")
  12. }
  13. ...
  14. }

这时候运营又要插个“活动”在第3个位置,但是只有VIP用户才显示,那我们又需要修改渲染部分和点击部分,还是单就点击事件来说,修改后代码如下:

  1. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  2. if indexPath.row == 0 {
  3. print("排行榜")
  4. } else if indexPath.row == 1 {
  5. print("任务")
  6. } else {
  7. if isVip {
  8. if indexPath.row == 2 {
  9. print("活动列表")
  10. } else if indexPath.row == 3 {
  11. print("个人资料")
  12. } else if indexPath.row == 4 {
  13. print("等级")
  14. } else if indexPath.row == 5{
  15. print("邀请奖励")
  16. }
  17. } else {
  18. if indexPath.row == 2 {
  19. print("个人资料")
  20. } else if indexPath.row == 3 {
  21. print("等级")
  22. } else if indexPath.row == 4{
  23. print("邀请奖励")
  24. }
  25. }
  26. }
  27. }

哇,看起来有一点乱了,况且这还是只有一个条件,如果有多个元素需要多个条件来控制,那每次需要修改的代码可就有点吓人了。

同样地,如果红色区域的部分需要调整,那么列表头内部的元素布局也需要修改大量代码。显然,硬编码的方式虽然直观,但在面对复杂多变的需求时显得有些捉襟见肘。

数据驱动 - 灵活布局

下面我们就使用数据驱动页面布局的方式再来实现这个页面。首先我们把页面的结构重新分割一下,将它们分割成更多更小的元素。

  • 导航栏:蓝色区域部分,这里仍然是导航栏的保留区域。
  • 用户信息:绿色部分,这里面包含了用户的基本信息。
  • VIP:紫色部分,VIP入口。
  • 钱包:橙色部分,钱包入口。
  • 其它列表:红色部分,其它样式相同但功能不同的入口。

这样分割之后呢,我们就只需要关注导航栏和列表就可以了,导航栏的UI已经固定且已经是最小元素,应该没有不会有什么变化,那么我们就把重点放到列表上。

每一个不同的区域都是一种类型的列表元素,那我们需要提前将所有的列表类型进行注册,代码如下:

  1. // 设置列表
  2. func addTableView() {
  3. tableView.frame = CGRect(x: 0, y: cs_navigationBarHeight, width: CS_SCREENWIDTH, height: CS_SCREENHIGHT - cs_navigationBarHeight)
  4. tableView.delegate = self
  5. tableView.dataSource = self
  6. tableView.backgroundColor = .white
  7. tableView.separatorStyle = .none
  8. self.view.addSubview(tableView)
  9. // 注册个人信息
  10. tableView.register(CSMeUserInfoCell.self, forCellReuseIdentifier: MeCellType.userInfo.rawValue)
  11. // 注册vip
  12. tableView.register(CSMeVipCell.self, forCellReuseIdentifier: MeCellType.vip.rawValue)
  13. // 注册钱包
  14. tableView.register(CSMeWalletCell.self, forCellReuseIdentifier: MeCellType.wallet.rawValue)
  15. // 普通列表
  16. tableView.register(CSMeNormalCell.self, forCellReuseIdentifier: MeCellType.normal.rawValue)
  17. }

这样列表内所有元素的样式就都已经注册完成了,接下来我们开始处理数据。

首先继承自NSObject创建一个数据模型CSMeRowItemModel,代码如下:

  1. class CSMeRowItemModel: NSObject {
  2. /// 标题
  3. var title:String?
  4. /// 图标
  5. var icon:UIImage?
  6. /// cell
  7. var reuseIdentifier:String?
  8. /// 点击回调
  9. var clickBlock:(()->Void)?
  10. }

该类里面有两个重要的数据 reuseIdentifier,列表cell的标识符,以及clickBlock一个闭包。

为了一步到位的介绍数据驱动布局的方式,我这里直接采用了分组的方式,因此还需要创建一个名为CSMeSectionItemModel的类,表示每组的数据,代码如下:

  1. class CSMeSectionItemModel: NSObject {
  2. /// 子数据
  3. var subArray:[CSMeRowItemModel] = []
  4. /// 是否显示组标题
  5. var showSectionHeader:Bool = false
  6. }

该类里面有一个主要数据就是subArray,里面保存了该组的item数据。

有了数据模型之后我们就可以开始构建数据列表了,为此我专门创建了一个CSMeConfigBuilder用来生成列表的页面数据。

生成Me页配置代码如下:

  1. /// 生成me页配置
  2. func buildMeConfig() -> [CSMeSectionItemModel] {
  3. var meConfig = [CSMeSectionItemModel]()
  4. // 个人信息
  5. let profileItem = buildProfileItem()
  6. meConfig.append(profileItem)
  7. // vip
  8. let vipItem = buildVipItem()
  9. meConfig.append(vipItem)
  10. // 钱包
  11. let walletItem = buildWalletItem()
  12. meConfig.append(walletItem)
  13. // 第一组
  14. let oneSectionItem = buildNormalOneSectionItem()
  15. meConfig.append(oneSectionItem)
  16. // 第二组
  17. // let twoSectionItem = buildNormalTwoSectionItem()
  18. // meConfig.append(twoSectionItem)
  19. //
  20. return meConfig
  21. }

而构建item列表的方法都大同小异,我就来列举两个吧,

构建钱包item,代码如下:

  1. // 生成钱包
  2. func buildWalletItem() -> CSMeSectionItemModel {
  3. let sectionItemModel = CSMeSectionItemModel()
  4. let walletItemModel = CSMeRowItemModel()
  5. walletItemModel.reuseIdentifier = MeCellType.wallet.rawValue
  6. walletItemModel.clickBlock = {
  7. // 钱包
  8. CSRouter.shared.route(path: CSRouterUrlMeWalletRecharge)
  9. // CSRouter.shared.route(path: CSRouterUrlShortVideoWallet)
  10. }
  11. sectionItemModel.subArray.append(walletItemModel)
  12. return sectionItemModel
  13. }

构建通用样式item,代码如下:

  1. // 第一组
  2. func buildNormalOneSectionItem() -> CSMeSectionItemModel {
  3. let sectionItemModel = CSMeSectionItemModel()
  4. // 排行榜
  5. let rankItemModel = CSMeRowItemModel()
  6. rankItemModel.reuseIdentifier = MeCellType.normal.rawValue
  7. rankItemModel.title = "Ranking"
  8. rankItemModel.icon = UIImage(named: "me_item_ranking_icon")
  9. rankItemModel.clickBlock = {
  10. // 检查 是否是游客登录
  11. if CSTouristHelper.shared.checkTouristLogin(loginSuccess: nil) {
  12. return
  13. }
  14. // 跳转排行榜
  15. CSRouter.shared.route(path: CSRouterUrlHomeRank)
  16. }
  17. sectionItemModel.subArray.append(rankItemModel)
  18. // 个人信息
  19. let personalInfoItemModel = CSMeRowItemModel()
  20. personalInfoItemModel.reuseIdentifier = MeCellType.normal.rawValue
  21. personalInfoItemModel.title = "Personal Information"
  22. personalInfoItemModel.icon = UIImage(named: "me_item_personal_info_icon")
  23. personalInfoItemModel.clickBlock = {
  24. // 个人页
  25. guard let uid = CSAccountManager.shared.account?.user?.id else { return }
  26. var params = [String:Any]()
  27. params["uid"] = uid
  28. CSRouter.shared.route(path: CSRouterUrlMeProfile,params: params)
  29. }
  30. sectionItemModel.subArray.append(personalInfoItemModel)
  31. // 等级
  32. let levelItemModel = CSMeRowItemModel()
  33. levelItemModel.reuseIdentifier = MeCellType.normal.rawValue
  34. levelItemModel.title = "Level"
  35. levelItemModel.icon = UIImage(named: "me_item_level_icon")
  36. levelItemModel.clickBlock = {
  37. //等级
  38. CSRouter.shared.route(path: CSRouterUrlMeLevel)
  39. }
  40. sectionItemModel.subArray.append(levelItemModel)
  41. // 邀请奖励
  42. let inviteItemModel = CSMeRowItemModel()
  43. inviteItemModel.reuseIdentifier = MeCellType.normal.rawValue
  44. inviteItemModel.title = "Rewards Invite"
  45. inviteItemModel.icon = UIImage(named: "me_item_invite_icon")
  46. inviteItemModel.clickBlock = {
  47. //邀请
  48. CSRouter.shared.route(path: CSRouterUrlMeInvite)
  49. }
  50. sectionItemModel.subArray.append(inviteItemModel)
  51. ....
  52. return sectionItemModel
  53. }

接下来在页面控制器内我们只需要读取配置列表,使用列表数据直接渲染列表。

读取配置列表:

  1. /// 配置
  2. let configBuiler = CSMeConfigBuilder()
  3. /// 配置列表
  4. var configList = [CSMeSectionItemModel]()
  5. func initData() {
  6. configList = configBuiler.buildMeConfig()
  7. }

使用列表数据渲染UI:

  1. func numberOfSections(in tableView: UITableView) -> Int {
  2. return configList.count
  3. }
  4. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  5. if section >= configList.count {
  6. CSAssert(false, "CSMeViewController section >= configList.count")
  7. return 0
  8. }
  9. let sectionItemModel = configList[section]
  10. return sectionItemModel.subArray.count
  11. }
  1. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  2. let sectionItemModel = configList[indexPath.section]
  3. let itemModel = sectionItemModel.subArray[indexPath.row]
  4. let cell = tableView.dequeueReusableCell(withIdentifier: itemModel.reuseIdentifier!, for: indexPath)
  5. cell.selectionStyle = .none
  6. // 个人信息
  7. if let userInfoCell = cell as? CSMeUserInfoCell {
  8. userInfoCell.renderUserInfo()
  9. }
  10. // 普通cell
  11. if let normalCell = cell as? CSMeNormalCell {
  12. normalCell.renderData(itemModel)
  13. }
  14. return cell
  15. }

只需要这样做,页面就会根据我们配置好的数据渲染出来了。接下来就是处理点击事件,这就更容易了,因为我们已经把事件和数据绑定到了一起,我们只需要获取对应的数据,然后来调用它的闭包,代码如下:

  1. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  2. let sectionItemModel = configList[indexPath.section]
  3. let itemModel = sectionItemModel.subArray[indexPath.row]
  4. itemModel.clickBlock?()
  5. }

我们不需要添加任何判断,就可以把点击事件对应到我们想要的功能。

而且当页面需要添加元素,或者隐藏元素,哪怕是动态的显示和隐藏元素,我们都只需要操作CSMeConfigBuilder里面构建生成页面数据的方法,而不需要修改任何UI,除非是增加新的样式。

结语

通过这个典型的“Me”页案例,我们分别使用硬编码和数据驱动布局来实现页面构建。通过对比可以发现,在实际开发过程中,使用数据驱动页面布局的方式更加灵活且更容易扩展。每一个小元素都拥有完整的功能,在添加或删除时,我们只需要对数据略微进行修改,而不需要大幅度修改约束代码或添加大量的条件判断。这不仅提高了开发效率,也增强了代码的可维护性。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/898667
推荐阅读
相关标签
  

闽ICP备14008679号