阅读视图
5454. 统计全 1 子矩形
解题思路
矩阵里每个点(i.j)统计他这行左边到他这个位置最多有几个连续的1,存为left[i][j]。然后对于每个点(i.j),我们固定子矩形的右下角为(i.j),利用left从该行i向上寻找子矩阵左上角为第k行的矩阵个数。每次将子矩阵个数加到答案中即可。
时间复杂度O(nnm),空间复杂度O(nm)。
代码
###cpp
class Solution {
public:
int numSubmat(vector<vector<int>>& mat) {
int n = mat.size();
int m = mat[0].size();
vector<vector<int> > left(n,vector<int>(m));
int now = 0;
for(int i=0;i<n;i++){
now = 0;
for(int j=0;j<m;j++){
if(mat[i][j] == 1) now ++;
else now = 0;
left[i][j] = now;
}
}
int ans = 0,minx;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
minx = 0x3f3f3f3f;
for(int k=i;k>=0;k--){
minx = min(left[k][j],minx);
ans += minx;
}
}
}
return ans;
}
};
用 SwiftUI 打造一个 iOS「设置」界面
用 SwiftUI 打造一个 iOS「设置」界面
在开发 iOS App 时,我们常常需要构建「设置页面」。原生的 Settings.bundle
虽然能提供系统级设置,但多数情况下我们更希望在应用内部实现一套「拟系统」的设置界面。
本文将带你从 0 到 1,用 SwiftUI 写出一个高度还原 iOS 设置风格的组件库,包括:
- 可复用的设置行组件(SettingRow)
- 分组容器(SettingsSection)
- 多类型行支持(开关、导航、输入框、详情、按钮等)
- 完整 Demo 界面(SettingsView)
最终效果如下 👇(几乎就是 iOS 设置的翻版):
(你可以运行代码自己体验,UI 风格完全遵循 iOS 官方人机界面规范)
1. 定义图标类型
我们首先需要一个「图标枚举」,为不同的设置项绑定系统 SF Symbols 图标和颜色。这样能确保代码整洁,也方便后续扩展:
enum SettingIconType {
case general, notifications, privacy, screen, battery, wifi, bluetooth, cellular
case language, keyboard, accessibility, storage, camera, photos
case appstore, safari, mail, messages, facetime, music
case resetSettings, exportData, deviceInfo
var icon: String {
switch self {
case .general: return "gear"
case .wifi: return "wifi"
case .battery: return "battery.100"
// 其他略...
default: return "gear"
}
}
var color: Color {
switch self {
case .wifi: return .blue
case .battery: return .green
case .resetSettings: return .red
default: return .gray
}
}
}
这里用到了 SF Symbols,几乎可以覆盖绝大多数系统图标需求。
2. 设置行类型
不同的行需要不同的交互:有的跳转,有的开关,有的输入文字。我们用枚举 SettingRowType
来抽象这些类型:
enum SettingRowType {
case navigation(action: () -> Void)
case toggle(binding: Binding<Bool>)
case detail(text: String, action: (() -> Void)? = nil)
case stepper(value: Binding<Double>, range: ClosedRange<Double>, step: Double = 1.0)
case picker(selection: Binding<String>, options: [String], action: (() -> Void)? = nil)
case textField(text: Binding<String>, placeholder: String = "")
case display(text: String)
case button(style: ButtonStyle = .normal, action: () -> Void)
enum ButtonStyle {
case normal, destructive, prominent
}
}
这种写法的好处是:所有 UI 类型都集中在一个枚举中,扩展时只需要新增 case。
3. 复用的行组件 SettingRow
接下来就是核心组件 —— SettingRow
。它接收一个 SettingIconType
(图标)、一个标题、一个 SettingRowType
(类型),自动渲染出对应的 UI。
struct SettingRow: View {
let iconType: SettingIconType
let title: String
let type: SettingRowType
var body: some View {
HStack(spacing: 12) {
Image(systemName: iconType.icon)
.foregroundColor(iconType.color)
.frame(width: 25, height: 25)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color(.systemGray5), lineWidth: 1)
)
Text(title)
.font(.system(size: 17))
Spacer()
rightContent
}
.padding(.horizontal, 16)
.frame(minHeight: 44)
.background(Color(.secondarySystemGroupedBackground))
.contentShape(Rectangle())
.onTapGesture { handleTap() }
}
}
核心逻辑在 rightContent
——它会根据不同的 SettingRowType
渲染出右侧的 Toggle、Chevron、Stepper、TextField 等控件。
4. 分组容器 SettingsSection
iOS 设置页面通常会分组显示,每组有 Header/Footer。我们写一个 SettingsSection
来包裹行组件:
struct SettingsSection<Content: View>: View {
let header: String?
let footer: String?
@ViewBuilder var content: Content
var body: some View {
VStack(alignment: .leading, spacing: 6) {
if let header = header {
Text(header.uppercased())
.font(.footnote)
.foregroundColor(Color(.secondaryLabel))
.padding(.horizontal)
}
VStack(spacing: 0) {
content
}
.background(Color(.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding(.horizontal)
if let footer = footer {
Text(footer)
.font(.footnote)
.foregroundColor(Color(.secondaryLabel))
.padding(.horizontal)
}
}
}
}
这样我们就能用类似 UIKit 的 TableView Section 的方式组织 UI。
5. 完整示例 SettingsView
最后,写一个完整页面示例,把 Wi-Fi、蓝牙、亮度、语言等都组合起来:
struct SettingsView: View {
@State private var wifiEnabled = true
@State private var bluetoothEnabled = false
@State private var brightness: Double = 50
@State private var deviceName = "我的 iPhone"
@State private var language = "简体中文"
var body: some View {
ScrollView {
VStack(spacing: 20) {
// 连接
SettingsSection(header: "连接") {
SettingRow.toggle(iconType: .wifi, title: "Wi-Fi", isOn: $wifiEnabled)
SettingRow.toggle(iconType: .bluetooth, title: "蓝牙", isOn: $bluetoothEnabled)
SettingRow.navigation(iconType: .cellular, title: "蜂窝网络") {
print("进入蜂窝网络")
}
}
// 显示与亮度
SettingsSection(header: "显示与亮度") {
SettingRow(iconType: .screen, title: "亮度", type: .stepper(value: $brightness, range: 0...100, step: 5))
}
// 通用
SettingsSection(header: "通用") {
SettingRow.navigation(iconType: .general, title: "关于本机") {
print("关于本机")
}
SettingRow.detail(iconType: .language, title: "语言与地区", detail: language) {
print("选择语言")
}
SettingRow(iconType: .keyboard, title: "设备名称", type: .textField(text: $deviceName, placeholder: "输入名称"))
}
// 设备信息
SettingsSection(header: "设备信息", footer: "这些信息用于识别您的设备") {
SettingRow.display(iconType: .deviceInfo, title: "型号", value: "iPhone 15 Pro")
SettingRow.display(iconType: .deviceInfo, title: "iOS 版本", value: "17.5.1")
}
// 重置
SettingsSection(footer: "重置设置不会删除数据") {
SettingRow.button(iconType: .resetSettings, title: "重置所有设置", style: .destructive) {
print("重置")
}
SettingRow.button(iconType: .exportData, title: "导出数据", style: .prominent) {
print("导出")
}
}
}
.padding(.vertical)
}
.background(Color(.systemGroupedBackground))
}
}
6. 总结
通过这种方式,我们实现了一个:
✅ 高度还原 iOS 系统设置的界面
✅ 可复用的 SettingRow
组件,支持导航/开关/输入等多种类型
✅ 分组化 SettingsSection
,完全符合系统风格
✅ 简单扩展,方便日后增加新类型
如果你正在开发一个需要「偏系统风格」的 App,这套代码可以作为你的基础设置模块,直接拿来用。
👉 源码已贴全,可以直接复制到项目中运行。
如果你觉得有帮助,点个赞 👍 或收藏 🔖,下次写 UI 时就能快速用上啦!
当我把前端条件加载做到极致
各位网友晚上好,相信大家都对使用type="module"和nomodule这种技巧有所耳闻。今天我将为大家分享我把这种技巧应用到极致的经验:4 条件加载方案。
什么是分条件加载
在前端构建中,我们通常需要进行 Babel 语法降级和 polyfill 插入等操作以兼容低版本浏览器。但这会导致一个问题:在现代浏览器中也会加载大量兼容性代码,无法享受浏览器原生支持的新特性带来的性能优势。
分条件加载的核心思想是:为不同能力的浏览器提供差异化的构建产物。最基础的实现就是利用 HTML5 标准中的type="module"和nomodule属性进行区分:
<!-- 仅供参考 -->
<script type="module" src="modern-bundle.js"></script>
<script nomodule src="legacy-bundle.js"></script>
这样在支持 ES Module 的浏览器中会加载type="module"的脚本,而不支持的浏览器会忽略这个标签并加载带有nomodule属性的脚本。通过这种方式,现代浏览器就能直接运行原生 class、ES Module、解构赋值等新特性,无需加载冗余的兼容性代码。
3 个条件的分条件加载
由于IE8与IE9之间有巨大的差异,因此在上述2个版本的构建产物之上,使用条件注释在分出第3个版本专用于IE8及以下。
<!-- 仅供参考 -->
<!--[if gte IE 9]><!-->
<script type="module" src="modern-bundle.js"></script>
<script nomodule src="legacy-bundle.js"></script>
<!--><![endif]-->
<!--[if lte IE 8]>
<script src="ie8-bundle.js"></script>
<![endif]-->
这样IE8及以下加载特制兼容包可以更加精简,其他版本也能避开恶心的兼容代码。
为什么需要分4条件加载
分3个版本似乎己经够用了,但是随着top-level-await
的出现,变得不够用了。使用type="module"和nomodule分版本的一个目的就是能使用原生esmodule。而top-level-top
出现后,无法用旧有的esmodule实现top-level-await
。因此只能使用传统方式,比如构建成AMD格式,用AMD加载器加载。因此我们可以进一步细分出一个版本。
现在我们有4个版本了
- IE8及以下
- 不支持原生esm的版本(chrome4~chrome60)
- 支持原生esm但不支持top-level-await(chrome61~chrome89)
- 原生支持top-level-await的浏览器(chrome90+)
注意哦,上面最新的版本是chrome90,现在都chrome130+了,随着浏览器的更新,我希望用新浏览器的用户能够直接运行新特性,因此我需要用一个较新的特性做区分,于是我选择了Promise.try。现在的4个版本改变为
- IE8及以下
- 不支持原生esm的版本(chrome4~chrome60)
- 支持原生esm但不支持Promise.try(chrome61~chrome127)
- 原生支持Promise.try的浏览器(chrome128+)
这里我选择了Promise.try是因为我还没有在公司推开es2025的使用,业务人员是不允许使用es2025的特性,代码中也必然不存在es2025的代码。等我在公司构建工具链中做完了es2025的降级处理,就会把用Promise.try判断,改成用更新的特性来判断。
<!-- 仅供参考 -->
<!--[if gte IE 9]><!-->
<script type="module">
if(Promise.try){__import__("xxxx")}else{__import__("yyyy")}
</script>
<script nomodule src="legacy-bundle.js"></script>
<!--><![endif]-->
<!--[if lte IE 8]>
<script src="ie8-bundle.js"></script>
<![endif]-->
最后我们看看打包后的运行效果。我们发现在最新浏览器中没有polyfill引用,也可以原生运行top-level-await。
总结
通过多条件加载,可以在全浏览器兼容的前提下,减少新式浏览器的加载内容大小。分4版本条件加载,是做到极致的水平。
SwiftUI TabView 源码解析
如何使用和自定义 SwiftUI 标签
戴上眼镜,起飞!影翎 A1 全景无人机首发评测
你大概率见过这样一幕——一台航拍无人机,身上绑着一部全景相机。早在 2022 年,影石就嗅到了这股创意的风,推出了适配大疆 Air2 系列的全景相机套件「瞳 Sphere」,让无人机玩家第一次尝到了 「全景飞行」 的滋味。
▲ 为了给无人机挂上全景相机,用户和品牌都绞尽了脑汁
而今天,故事迎来了升级版:全球首款全景无人机—— 影翎 A1 登场。毫不意外,背后那双最熟悉的推手,依然来自影石。
▲套装包含:无人机本体、体感遥控手柄、飞行眼镜及电池
无人机本体的外观非常科幻。正面竖向排列的视觉避障模组就像电影中的外星生物,而上下两边的半球形凸起就是全景相机模组,一上一下的镜头放置方式和「瞳 Sphere」相同。
▲ 开机自动展开起落架,避免底部的全景镜头接触地面
让我成为一只鸟,在空中自由探索
既然是影石孵化的品牌,身上自然也流淌着影石的血液,尤其是全景合成技术。
影翎 A1 能够在飞行过程中实时合成全景影像,结合经特别设计的镜头布局,它能够实现出色的「隐形」效果,在飞行眼镜中环顾四周也不会看到机身和螺旋桨的踪迹。
▲ 戴着飞行眼镜转头可以轻松变换视线方向
想象你正在北京环球影城的哈利波特禁忌之旅上,飞车极速前进时,四周的景象尽收眼底。即便飞车在前进,你的视角却可以任意转动,就像是身临其境,飞车的方向并不限制你的视野。
影翎 A1 带给我的就是这种自由的体验,仿佛化身为一只在天空中翱翔的鸟,一边飞行,一边自由探索眼前的美景。这样的飞行体验,非常适合在各大景区应用——工作人员摇身一变成为无人机驾驶员,游客则可以「坐」在无人机上,随心所欲地欣赏四周的风光。
全景飞行的模式使其操作方式有别于传统 FPV 无人机——飞行方向只由手柄控制,不再与头显的视角同步。当你的视角与飞行方向不一致时,头显屏幕上会自动弹出一个悬浮窗,实时显示无人机前方的影像。而你只需轻轻转动手柄上的滚轮,就能迅速回到飞行器前方的视角并调整飞行方向,避免盲飞的风险。
其实对于新手来说是很好上手的,但如果你以前就飞过 FPV,那我建议你放下所有的经验,像个新手一样享受这份空中飞行的自由感。
很好玩,也很好拍
在这次体验的前半部分,我完全沉迷在自由的观光感受里了,直到我想起它还是一部能够拍摄全景视频的生产力工具,然后马上就发现了全景无人机的妙处——先专心飞,再随意构图。
影翎 A1 能够录制最高 8K@30fps 规格的全景视频(可惜由于是早期工程样机,我们只体验到了 5.7K@50fps 的视频规格),然后在后期软件里自由调整视角,设置关键帧打点实现一些高难度的镜头运动。
这意味着,在前期拍摄时飞手无需纠结构图与运镜。只要专注于操控飞行,确保无人机与拍摄对象的距离与相对位置即可。
以前,拍摄这些镜头时需要操控 FPV 无人机进行翻转、甩尾等高难度动作,还要冒着很大的炸机风险。但现在,只需在后期软件中拖动画面、设置关键帧,便能轻松重现这些效果。
也难怪早就有玩家们在无人机上绑全景相机了,确实好用。
让航拍过程成为一种享受
爱范儿觉得影翎 A1 会是一款彻底改变过往飞行体验的产品。
作为全球首款一体式全景无人机,影翎 A1 融合了手势体感控制摇杆、实时合成全景视频和全方位观测的飞行眼镜,为飞行者带来一种前所未有的体验。它让每个人都能在空中自由探索,尽情欣赏大自然的鬼斧神工。
对于影像创作者来说,这样的一体化设计也比曾经专机专用的外挂式配件来得更加可靠,无论是飞行操控还是拍摄体验都更上一层。
最重要的是,影翎 A1 突破了传统飞行的束缚,它解放了创作者们 「机不可失,失不再来」 的焦虑,将他们的飞行体验提升到全新的高度,让航拍的飞行过程本身也成为一种享受。
给创作者们提供了大胆飞行、自由创作的底气,这才是影翎 A1 的独特魅力所在。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。