SwiftUI 让视图自适应高度的 6 种方法(四)

概览
在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。

如上图所示,无论顶部 TabView 容器里子视图高度如何变化,TabView 本身的高度都能“随遇而安”。如何用最简单、最现代化、最有趣且最切中要害的方法让容器尺寸与子视图的高度“如影随形”呢?
在本篇博文中,您将学到如下内容:
- 概览
- 9. 最“相得益彰”的实现:自定义布局 Layout
- 9.1 重装上阵 Layout
- 9.2 “奇怪的” TabView
- 9.3 MaxHeightLayout 的实现
- 总结
相信学完本课后,小伙伴们必能脑洞大开、格局打开,用“千姿百态”的方法让问题的解决一发入魂、九转功成!
那还等什么呢?Let‘s go!!!😉
9. 最“相得益彰”的实现:自定义布局 Layout
在一口气介绍完上面 5 种“五花八门”的实现之后,我们完全可以“鸣金收兵”。但是为了面面俱到,我们最后还是决定用自定义布局 Layout 来为整个系列博文画一个圆满的句号。
9.1 重装上阵 Layout
所谓自定义布局 Layout,其实就是创建一款遵守 Layout 协议的“容器”(严格说应该是视图集合 Collection of views),然后“恣意”为内部的子视图“排兵布阵”:

为什么说用 Layout 这种方法更加“鞭辟入里”呢?因为这是处理多个同一层级子视图布局最自然的方式。
大家回忆一下:我们是将所有喜爱的成语用 ForEach 挨个放在 TabView 容器里的,在父容器中对它们的布局“运筹帷幄”是理所当然的事。
关于自定义布局的进一步介绍,请小伙伴们移步如下链接观赏精彩的文章:
- SwiftUI 打造一款收缩自如的 HStack(四):Layout 自定义布局
9.2 “奇怪的” TabView
我们的目标是创建一个通用自定义布局 MaxHeightLayout,然后实时计算出所有子视图中最高的 Height。由于 MaxHeightLayout 是作为一个“容器”放在 TabView 中的,我们必须显式设置 TabView 的高度,而不能通过设置 MaxHeightLayout 的高度来间接影响 Tabview。
为什么会这样呢?这是由于 TabView 自身的特殊性质造成的。
比如在下面的代码中,我们在 TabView 里放置了一个高度为 200 的圆形:
TabView {Circle().foregroundStyle(.green.gradient).frame(height: 200)
}
.tabViewStyle(.page)
尽管我们将内部圆形的高度设置为 200,明确“暗示” TabView 把自己的高度也做出相应调整 ,但 TabView 还是会无动于衷:

要想 TabView 能够充分容纳高度为 200 的圆形,我们必须将 TabView 的高度显式设置为 200:
TabView {Circle().foregroundStyle(.green.gradient)
}
.tabViewStyle(.page)
.frame(height: 200)

换句话说,TabView 不会站在子视图的角度考虑问题,它会完全忽略子视图尺寸的提议,“一意孤行”。
9.3 MaxHeightLayout 的实现
上面讨论的结果迫使我们必须让自定义布局 MaxHeightLayout 想办法将计算产生的最大高度传递向外给 TabView 才行。
有很多种方法可以达到目的,这里我们采用最简单的一种:绑定(Binding)。
struct MaxHeightLayout: Layout {var spacing: CGFloat?@Binding var maxHeight: CGFloatfunc sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {let proposalWidth = proposal.width!let idealViewSizes = subviews.map { $0.sizeThatFits(.init(width: proposalWidth / CGFloat(subviews.count), height: nil)) }let totalHeight = idealViewSizes.map {$0.height}.max() ?? 0.0// 防止反复赋值造成渲染循环if totalHeight > maxHeight {maxHeight = totalHeight}return CGSize(width: proposalWidth, height: totalHeight)}private func calcSpaces(subviews: Subviews) -> [CGFloat] {if let spacing {[CGFloat](repeating: spacing, count: subviews.count - 1)} else {subviews.indices.map { idx inguard idx < subviews.count - 1 else { return 0 }return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)}}}func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {let spaces = calcSpaces(subviews: subviews)var point = CGPoint(x: bounds.minX, y: bounds.minY)let subviewWidth = bounds.width / CGFloat(subviews.count)for idx in subviews.indices {subviews[idx].place(at: point, proposal: .init(width: subviewWidth - spaces[idx], height: maxHeight))if idx < subviews.count - 1 {point.x += subviewWidth - spaces[idx]}}}
}
在上面的代码中,我们主要做了这么几件事:
- 让 MaxHeightLayout “容器”中每个子视图的宽都平分容器的宽度;
- 用 calcSpaces 方法计算子视图间的空隙,并确保 placeSubviews 方法在布局子视图时应用它们;
- 只在必要时更新 maxHeight 绑定的值(totalHeight > maxHeight 时),这是避免“递归渲染”的重要手段;
最后,只要将 TabView 中原来内层的 ForEach 循环以及相关逻辑放在 MaxHeightLayout 里就可以啦:
Section("喜爱的成语") {TabView {ForEach(likeIdioms.chunked(into: 2), id: \.self) { idiomChunk inVStack {MaxHeightLayout(maxHeight: $maxHeight) { ForEach(idiomChunk) { idiom inlikeIdiomCard(idiom)}if idiomChunk.count < 2 {Rectangle().foregroundStyle(.clear)}}Spacer()}}}.tabViewStyle(.page).frame(height: maxHeight).padding(.bottom, 8)
}
运行代码可以发现结果和其它的实现毫无二致!

借助自定义布局 Layout 的灵活性,我们可以非常轻松的改变 TabView 中成语显示的数量,比如改为 3 列也不在话下:

至此,我们圆满完成了本系列博文中的所有任务。秃头小伙伴们还不赶紧给自己一个大大的赞吧!爱你们哦!❤
想要进一步系统地学习 Swift 开发的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:

- 《Swift 语言开发精讲》
总结
在本篇博文中,我们介绍了如何使用自定义布局 Layout 来实现 SwiftUI 视图高度的“遥相呼应”,精彩的大结局小伙伴们不容错过哦!
感谢观赏,再会啦!😎
相关文章:
SwiftUI 让视图自适应高度的 6 种方法(四)
概览 在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。 如上图所示,无论顶部 TabView 容器里子视图…...
机器学习中的梯度下降是什么意思?
梯度下降(Gradient Descent)是机器学习中一种常用的优化算法,用于最小化损失函数(Loss Function)。通过迭代调整模型参数,梯度下降帮助模型逐步逼近最优解,从而提升模型的性能。 1.核心思想 梯…...
opencv-显示图片
安装软件 sudo apt install python3 //确保虚拟机只有python3 ln -sf /usr/bin/python3.6 /usr/bin/python sudo apt install python3-pip pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple pip install opencv-contrib-python -i https://pypi.tuna…...
sap关账+策略模式(避免大量if elseif)
旧代码 Transactional(rollbackFor Exception.class)public AjaxResult purchaseOrderReceiptOutSourceAfterSapCloseAccountingPeriod(Long id) {SysPurorderPostingLog sysPurorderPostingLog sysPurorderPostingLogMapper.selectSysPurorderPostingLogById(id);if (Object…...
EverArt MCP 服务器安装调试笔记 -cline
EverArt MCP 服务器安装调试笔记 问题描述 用户在使用 EverArt MCP 服务器时遇到报错:“MCP error -1: Connection closed”。 调试过程 检查配置文件 cline_mcp_settings.json: 确认 everart 服务器的配置信息,包括 command、args 和 env 是否正确。…...
1035.不相交的线
1035.不相交的线 力扣题目链接(opens new window) 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足: nums1[i] nums2[j]且绘制的直线…...
Django-ORM-select_related
Django-ORM-select_related 作用使用场景示例无 select_related 的查询有 select_related 的查询 如何理解 "只发起一次查询,包含所有相关作者信息"1. select_related 的工作原理2. 具体示例解析3. 为什么只发起一次查询 数据库中的books量巨大࿰…...
2001-2023年上市公司数字化转型年报词频统计(年报词频统计和MDA词频统计两种方式)(吴非、赵宸宇、甄红线300+关键词三种方法)
2001-2023年上市公司数字化转型年报词频统计(年报词频统计和MD&A词频统计两种方式)(吴非、赵宸宇、甄红线300关键词三种方法) 1、时间:2001-2023年 2、来源:上市公司年报 3、参考文献: …...
IO多路复用实现并发服务器
一.select函数 select 的调用注意事项 在使用 select 函数时,需要注意以下几个关键点: 1. 参数的修改与拷贝 readfds 等参数是结果参数 : select 函数会直接修改传入的 fd_set(如 readfds、writefds 和 exceptfds…...
React 如何实现组件懒加载以及懒加载的底层机制
前言 在现代前端开发中,性能优化始终是一个核心课题。React 作为当下流行的前端库之一,提供了一些非常有用的工具和技术来提升应用的性能,其中懒加载(Lazy Loading)就是一项不可忽视的重要技术。通过懒加载࿰…...
《论语别裁》第01章 学而(22) 种瓜者
下面一节,等于一个结论: 曾子曰:慎终追远,民德归厚矣。 古人对于这一句的解释,我也有点意见。拿孝道来讲,过去讲中国文化的孝道,本来很重要,我们看历史上给皇帝的奏议,常…...
如何设置GET请求的参数?
在使用PHP爬虫时,设置GET请求的参数是与目标网站交互的关键步骤。通过正确设置GET请求的参数,可以向目标网站发送查询请求并获取相应的数据。以下是如何在PHP中设置GET请求参数的详细说明,包括使用cURL和GuzzleHttp库的示例。 一、使用cURL设…...
C++20 模块:告别头文件,迎接现代化的模块系统
文章目录 引言一、C20模块简介1.1 传统头文件的局限性1.2 模块的出现 二、模块的基本概念2.1 模块声明2.2 模块接口单元2.3 模块实现单元 三、模块的优势3.1 编译时间大幅减少3.2 更好的依赖管理3.3 命名空间隔离 四、如何使用C20模块4.1 编译器支持4.2 示例项目4.3 编译和运行…...
SpringBoot集成Swagger指南
在Spring Boot项目中集成Swagger可以帮助你自动生成API文档,并且提供一个交互式的UI界面,方便开发者测试和调试API。以下是集成Swagger的步骤: 1. 添加Swagger依赖 首先,在你的pom.xml文件中添加Swagger的依赖项。通常使用的是s…...
有必要使用 Oracle 向量数据库吗?
向量数据库最主要的特点是让传统的只能基于具体值/关键字的数据检索,进化到了可以直接基于语义的数据检索。这在AI时代至关重要! 回到标题问题:是否有必要使用 Oracle 向量数据库? 这实际还要取决于你的具体应用需求。 客观来讲…...
仅仅使用pytorch来手撕transformer架构(3):编码器模块和编码器类的实现和向前传播
仅仅使用pytorch来手撕transformer架构(2):编码器模块和编码器类的实现和向前传播 往期文章: 仅仅使用pytorch来手撕transformer架构(1):位置编码的类的实现和向前传播 最适合小白入门的Transformer介绍 仅仅使用pytorch来手撕transformer…...
rust语言match模式匹配涉及转移所有权Error Case
struct S{data:String, }//注意:因为String默认是移动语义,从而决定结构体S也是移动语义,可采用(1)或(2)两种方法解决编译错误;关键思路:放弃获取结构体S的字段data的所有权,改为借用。fn process(s_ref:&a…...
小肥柴慢慢手写数据结构(C篇)(4-3 关于栈和队列的讨论)
小肥柴慢慢学习数据结构笔记(C篇)(4-3 关于栈和队列的讨论) 目录1 双端栈/队列2 栈与队列的相互转化2-1 栈转化成队列2-2 队列转化成栈 3 经典工程案例3-1 生产者和消费者模型(再次重温环形缓冲区)3-2 MapR…...
大模型在甲状腺癌诊疗全流程预测及方案制定中的应用研究
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与创新点 1.3 国内外研究现状 二、大模型预测甲状腺癌的理论基础 2.1 甲状腺癌相关医学知识 2.2 大模型技术原理与特点 2.3 大模型在医疗领域的应用潜力 三、术前预测方案 3.1 预测模型构建 3.1.1 数据收集与预处理 …...
java-单列模式-final-继承-多态
内存存储区域 引用变量和普通变量引用变量放在栈中,基本数据类型的内容是在堆内存中。 对象放在堆内存中,其引用变量放在栈中,指向堆内存存放对象的地址。 静态变量放在静态区中,静态变量在程序的执行始中中分配一次,…...
Python:正则表达式
正则表达式的基础和应用 一、正则表达式核心语法(四大基石) 1. 元字符(特殊符号) 定位符 ^:匹配字符串开始位置 $:匹配字符串结束位置 \b:匹配单词边界(如 \bword\b 匹配…...
网络通信中的带宽(Bandwidth)概念
在计算机网络中,带宽是指单位时间内可以传输的数据量,通常以比特每秒(bps)或字节每秒(Bps)为单位。 1. 理论计算 链路带宽:链路带宽是指网络链路的物理传输能力,通常由网络设备的规…...
基于杀伤链的勒索软件控制框架
40s说清楚勒索软件如何工作 基于杀伤链的勒索软件控制框架开发了4种缓解策略(预防、阻止、检测&响应、重建),覆盖18个控制域90项控制措施,以正确管理与勒索软件攻击杀伤链各阶段相关的风险。 注:本文节选出自《基于杀伤链的勒索软件防御指…...
Windows编程----结束进程
进程有启动就有终止,通过CreateProcess函数可以启动一个新的子进程,但是如何终结子进程呢?主要有四种方法: 通过主线程的入口函数(main函数、WinMain函数)的return关键字终止进程 一个应用程序只有一个入…...
三、Docker 集群管理与应用
(一)项目案例 1、准备主机 (1)关闭防火墙,或者开放TCP端口2377(用于集群管理通信)、TCP/UPD端口7946(用于节点之间的通信)、UDP端口4789(用于overlay网络流…...
无标签数据增强+高效注意力GAN:基于CARLA的夜间车辆检测精度跃升
目录 一、摘要 二、引言 三、框架 四、方法 生成合成夜间数据 昼夜图像风格转换 针对夜间图像的无标签数据增强技术 五、Coovally AI模型训练与应用平台 六、实验 数据 图像风格转换 夜间车辆检测和分类 结论 论文题目:ENHANCING NIGHTTIME VEHICLE D…...
SqlSugar 进阶之原生Sql操作与存储过程写法 【ORM框架】
系列文章目录 🎀🎀🎀 .NET开源 ORM 框架 SqlSugar 系列 🎀🎀🎀 文章目录 系列文章目录一、前言 🍃二、用法介绍三、方法列表四、使用案例五、调用存储过程六、in参数用法七、SqlServer带Go的脚…...
NO.33十六届蓝桥杯备战|函数|返回值|声明|调用|引用|函数重载(C++)
返回值 我们在设计的函数的时候,函数在经过计算后,有时候需要带回⼀些计算好的数据,这时候往往使⽤return 来返回,这⾥我们就讨论⼀下使⽤ return 返回。 return 后边可以是⼀个数值,也可以是⼀个表达式,…...
5G工业路由器赋能无人码头,港口物流智能化管理
全球贸易发展促使港口需提升运营效率,传统港口面临诸多难题,无人码头成为转型关键方向。5G 工业路由器为其提供有力通信支持,引领港口物流变革。 随着无人码头建设在全球兴起,如荷兰鹿特丹港、中国上海洋山港等。码头作业设备需实…...
机试准备第14天
首先进行树的学习。树的存储分为链式存储与顺序存储。完全二叉树是可以顺序存储的,将各个节点从上往下,从左往右存储。 第一题是找位置,好兄弟给的一道题,一遍过了。 #include <stdio.h> #include <map> #include &…...
