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

news2024/11/16 1:53:55

引言

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

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

数据驱动页面布局

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

案例

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

硬编码 - 直观布局

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

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

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

    /// 列表
    let tableView = UITableView(frame: .zero, style: .plain)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        addNavigationBar()
        addTableView()
        addTableHeaderView()
    }
    
    // 设置导航栏
    func addNavigationBar() {
        addCustomNavigationBar()
    }
    
    // 设置列表
    func addTableView() {
        tableView.frame = CGRect(x: 0, y: cs_navigationBarHeight, width: CS_SCREENWIDTH, height: CS_SCREENHIGHT - cs_navigationBarHeight)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.backgroundColor = .white
        tableView.separatorStyle = .none
        self.view.addSubview(tableView)
    }
    
    // 设置列表头
    func addTableHeaderView() {
        let headerView = PHMeHeaderView()
        headerView.frame = CGRect(x: 0, y: 0, width: CS_SCREENWIDTH, height: 450.0)
        tableView.tableHeaderView = headerView
    }

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

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

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

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

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

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

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

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.row == 0 {
            print("排行榜")
        } else if indexPath.row == 1 {
            print("任务")
        } else {
            if isVip {
                if indexPath.row == 2 {
                    print("活动列表")
                } else if indexPath.row == 3 {
                    print("个人资料")
                } else if indexPath.row == 4 {
                    print("等级")
                } else if indexPath.row  == 5{
                    print("邀请奖励")
                }
            } else {
                if indexPath.row == 2 {
                    print("个人资料")
                } else if indexPath.row == 3 {
                    print("等级")
                } else if indexPath.row  == 4{
                    print("邀请奖励")
                }
            }
        }
    }

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

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

数据驱动 - 灵活布局

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

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

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

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

    // 设置列表
    func addTableView() {
        tableView.frame = CGRect(x: 0, y: cs_navigationBarHeight, width: CS_SCREENWIDTH, height: CS_SCREENHIGHT - cs_navigationBarHeight)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.backgroundColor = .white
        tableView.separatorStyle = .none
        self.view.addSubview(tableView)
        // 注册个人信息
        tableView.register(CSMeUserInfoCell.self, forCellReuseIdentifier: MeCellType.userInfo.rawValue)
        // 注册vip
        tableView.register(CSMeVipCell.self, forCellReuseIdentifier: MeCellType.vip.rawValue)
        // 注册钱包
        tableView.register(CSMeWalletCell.self, forCellReuseIdentifier: MeCellType.wallet.rawValue)
        // 普通列表
        tableView.register(CSMeNormalCell.self, forCellReuseIdentifier: MeCellType.normal.rawValue)

    }

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

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

class CSMeRowItemModel: NSObject {
    /// 标题
    var title:String?
    /// 图标
    var icon:UIImage?
    /// cell
    var reuseIdentifier:String?
    /// 点击回调
    var clickBlock:(()->Void)?
}

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

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

class CSMeSectionItemModel: NSObject {
    /// 子数据
    var subArray:[CSMeRowItemModel] = []
    /// 是否显示组标题
    var showSectionHeader:Bool = false
}

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

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

生成Me页配置代码如下:

    /// 生成me页配置
    func buildMeConfig() -> [CSMeSectionItemModel] {
        var meConfig = [CSMeSectionItemModel]()
        // 个人信息
        let profileItem = buildProfileItem()
        meConfig.append(profileItem)
        
        // vip
        let vipItem = buildVipItem()
        meConfig.append(vipItem)
        
        // 钱包
        let walletItem = buildWalletItem()
        meConfig.append(walletItem)

        // 第一组
        let oneSectionItem = buildNormalOneSectionItem()
        meConfig.append(oneSectionItem)
        
        // 第二组
//        let twoSectionItem = buildNormalTwoSectionItem()
//        meConfig.append(twoSectionItem)
//        
        return meConfig
    }

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

构建钱包item,代码如下:

    // 生成钱包
    func buildWalletItem() -> CSMeSectionItemModel {
        let sectionItemModel = CSMeSectionItemModel()
        let walletItemModel = CSMeRowItemModel()
        walletItemModel.reuseIdentifier = MeCellType.wallet.rawValue
        walletItemModel.clickBlock = {
            // 钱包
            CSRouter.shared.route(path: CSRouterUrlMeWalletRecharge)
//            CSRouter.shared.route(path: CSRouterUrlShortVideoWallet)
        }
        sectionItemModel.subArray.append(walletItemModel)
        return sectionItemModel
    }

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

    // 第一组
    func buildNormalOneSectionItem() -> CSMeSectionItemModel {
        let sectionItemModel = CSMeSectionItemModel()
        // 排行榜
        let rankItemModel = CSMeRowItemModel()
        rankItemModel.reuseIdentifier = MeCellType.normal.rawValue
        rankItemModel.title = "Ranking"
        rankItemModel.icon = UIImage(named: "me_item_ranking_icon")
        rankItemModel.clickBlock = { 
            // 检查 是否是游客登录
            if CSTouristHelper.shared.checkTouristLogin(loginSuccess: nil) {
                return
            }
            // 跳转排行榜
            CSRouter.shared.route(path: CSRouterUrlHomeRank)
        }
        sectionItemModel.subArray.append(rankItemModel)
        // 个人信息
        let personalInfoItemModel = CSMeRowItemModel()
        personalInfoItemModel.reuseIdentifier = MeCellType.normal.rawValue
        personalInfoItemModel.title = "Personal Information"
        personalInfoItemModel.icon = UIImage(named: "me_item_personal_info_icon")
        personalInfoItemModel.clickBlock = {
            // 个人页
            guard let uid = CSAccountManager.shared.account?.user?.id else { return }
            var params = [String:Any]()
            params["uid"] = uid
            CSRouter.shared.route(path: CSRouterUrlMeProfile,params: params)
        }
        sectionItemModel.subArray.append(personalInfoItemModel)
        // 等级
        let levelItemModel = CSMeRowItemModel()
        levelItemModel.reuseIdentifier = MeCellType.normal.rawValue
        levelItemModel.title = "Level"
        levelItemModel.icon = UIImage(named: "me_item_level_icon")
        levelItemModel.clickBlock = {
            //等级
            CSRouter.shared.route(path: CSRouterUrlMeLevel)
        }
        sectionItemModel.subArray.append(levelItemModel)
        // 邀请奖励
        let inviteItemModel = CSMeRowItemModel()
        inviteItemModel.reuseIdentifier = MeCellType.normal.rawValue
        inviteItemModel.title = "Rewards Invite"
        inviteItemModel.icon = UIImage(named: "me_item_invite_icon")
        inviteItemModel.clickBlock = {
            //邀请
            CSRouter.shared.route(path: CSRouterUrlMeInvite)
        }
        sectionItemModel.subArray.append(inviteItemModel)
    ....
        return  sectionItemModel
}

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

读取配置列表:

    /// 配置
    let configBuiler = CSMeConfigBuilder()
    /// 配置列表
    var configList = [CSMeSectionItemModel]()


    func initData() {
        configList = configBuiler.buildMeConfig()
    }

使用列表数据渲染UI:

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

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

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

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

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

结语

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1945690.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

接入百度文心一言API教程

然后,编辑文章。点击AI识别摘要,然后保存即可 COREAIPOWER设置 暂时只支持经典编辑器.古腾堡编辑器等几个版本后支持.在比期间,你可以自己写点摘要 摘要内容 AL识别摘要 清空 若有收获,就点个赞吧 接入文心一言 现在百度文心一言&…

php-fpm如何配置max_children参数

前言 略 php-fpm 资源耗尽 php-fpm 的子进程耗尽的时: 会导致 502 出现nginx 出现错误日志 2024/07/18 20:19:10 [crit] 36390#0: *1402471 connect() to unix:/tmp/php-cgi-81.sock failed (2: No such file or directory) while connecting to upstream, cli…

OpenHarmony 入门——初识JS/ArkTS 侧的“JNI” NAPI 常见的函数详解(二)

引言 前面一篇文章OpenHarmony 入门——初识JS/ArkTS 侧的“JNI” NAPI(一)介绍了NAPI的基础理论知识,今天重点介绍下NAPI中重要的函数。 一、Native 侧的NAPI的相关的C函数 以下面一段代码为例介绍下主要函数的功能和用法。 napi_value …

前端网页打开PC端本地的应用程序实现方案

最近开发有一个需求,网页端有个入口需要跳转三维大屏,而这个大屏是一个exe应用程序。产品需要点击这个入口,并打开这个应用程序。这个就类似于百度网盘网页跳转到PC端应用程序中。 这里我们采用添加自定义协议的方式打开该应用程序。一开始可…

Elasticsearch-RestAPI --学习笔记

RestAPI ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。 官方文档地址: Elasticsearch Clients | Elastic 以下关于RestAPI 的说明都是基于老版本客户端 初始化RestClient 1&…

英语科技写作 希拉里·格拉斯曼-蒂(英文版)pdf下载

下载链接: 链接1:https://pan.baidu.com 链接2:/s/1fxRUGnlJrKEzQVF6k1GmBA 提取码:b69t 由于是英文版,可能有些看着不太方便,可以在网页版使用以下软件中英文对照着看,看着更舒服,…

Docker核心技术:Docker原理之Cgroups

云原生学习路线导航页(持续更新中) 本文是 Docker核心技术 系列文章:Docker原理之Cgroups,其他文章快捷链接如下: 应用架构演进容器技术要解决哪些问题Docker的基本使用Docker是如何实现的 Docker核心技术:…

扭蛋机潮玩小程序搭建,扭蛋机行业的创新

在当下潮玩市场中,扭蛋机具有盲盒的未知性和惊喜体验感,商品丰富,并且价格相对低廉,获得了极高的人气。年轻人开始对扭蛋机逐渐“上头”,为了扭到喜欢的商品不断地进行复购下单,在这场随机性的扭蛋游戏中&a…

Java语言程序设计——篇七(1)

🌿🌿🌿跟随博主脚步,从这里开始→博主主页🌿🌿🌿 继承 类的继承实战演练 方法覆盖实战演练 🍑super关键字实战演练 调用父类的构造方法 类的继承 通过类的继承方式,可以…

【Qt】QWidget核心属性相关API

目录 一. enabled——是否可用 二. geometry——几何位置 window frame 三. windowTitle——窗口标题 四. windowIcon——窗口图标 ​qrc文件 五. windowOpacity——透明度 六. cursor——光标 自定义光标 七. font——字体 八. toolTip——提示栏 九. focusPolic…

git免密推送代码至仓库gitee/github 常用命令

代码托管 gitee github gitclone 1 生成/添加SSH公钥 ssh-keygen -C "***qq.com"2 gitee添加公钥 查看公钥 cat ~/.ssh/id_rsa.pub然后再gitee添加 3 验证 gitee ssh -T gitgitee.comgithub ssh -T gitgithub.comgitclone:无法测试&#xff0c…

MSSQL注入前置知识

简述 Microsoft SQL server也叫SQL server / MSSQL,由微软推出的关系型数据库,默认端口1433 常见搭配C# / .net IISmssql mssql的数据库文件 数据文件(.mdf):主要的数据文件,包含数据表中的数据和对象信息…

Vue3可媲美Element Plus Tree组件开发之append节点

在前面的章节,我们完成了可媲美Element Plus Tree组件的基本开发。通过实现各种计算属性,tree数据状态变化引起的视图更新被计算属性所接管了,无需我们再手动做各种遍历、查找以及手动监听操作,这样后续开发高级功能变得易如反掌啦…

插入和选择排序

1.1直接插入排序 void InsertSort(int* a, int n) {for (int i 1; i < n - 1; i) {//i的范围要注意的&#xff0c;防止指针越界int end i;int tmp a[end 1];while (end>0) {if (tmp< a[end]) {a[end 1] a[end];//小于就挪动&#xff0c;虽然会覆盖后面空间的值…

Qt Creator平台编译snmp++

声明 &#xff1a;本文的大部分资源参考自文章&#xff0c;编译snmp的方法我也是在这里学习的&#xff0c;结合自己的需求&#xff0c;做了snmp和Agent的混合编译。需要了解更多的详情可以点击链接去看原文&#xff0c;我总结了自己的编译过程&#xff0c;并写下此文作为一个回…

Springboot+vue自制可爱英语日记系统-XD动画测试版

目录 项目背景与愿景 项目流程 需求分析 设计之美 技术实现 部署策略 未来展望 项目寄语 项目预览 项目页面展现 引导页(3张) 首页 日记模块 日记模块-写日记 信箱模块 回收箱模块 前端开发 前端开发概述 关键技术选型 开发流程 后端开发 后端开发概述 …

【算法/训练】:动态规划

一、路径类 1. 字母收集 思路&#xff1a; 1、预处理 对输入的字符矩阵我们按照要求将其转换为数字分数&#xff0c;由于只能往下和往右走&#xff0c;因此走到&#xff08;i&#xff0c;j&#xff09;的位置要就是从&#xff08;i - 1&#xff0c; j&#xff09;往下走&#x…

C++笔记3:基类指针delete子类对象的内存泄漏问题

根据《effective C》第7章所述&#xff0c;new的一个子类对象赋值给基类指针delete的时候为了防止子类的析构函数没有调用要在基类的析构函数加上virtual 关键字&#xff1a; #include <stdint.h> #include <iostream> #include <iomanip> #include <vec…

零代码实现GIS视效提升,一键添加体积云体积雾

在三维GIS开发中&#xff0c;场景的真实感和高效性始终是用户的核心需求。为此&#xff0c;山海鲸可视化提供了完美的解决方案。这款免费可视化工具不仅支持多种GIS影像协议&#xff08;TMS、WMS、WMTS等&#xff09;&#xff0c;还可以一键添加体积云和体积雾效果&#xff0c;…

FastGPT 知识库搜索测试功能解析(一)

本文以 FastGPT 知识库的搜索测试功能为入口,分析 FastGPT 的知识检索流程。 一、搜索功能介绍 1.1 整体介绍 搜索测试功能包含三种类型:语义检索、全文检索、混合检索。 语义检索:使用向量进行文本相关性查询,即调用向量数据库根据向量的相似性检索; 全文检索:使用…