iOS 界面字体回退系统原生 + iPad 适配 + 登录列宽

- 移除自定义 UI 字体,界面统一系统原生:删 BrandFonts.swift(Orbit/Maple woff2 运行期 CTFontManager 注册 + UIKit 外观代理)+ AppDelegate 的 register 调用 + CDropApp 全局 .environment(\.font, .oBody);9 视图 .font(.o语义) → .font(.语义) 系统语义(日志 LogView 消息行本就 .caption.monospaced() 原生等宽);删 Sources/Fonts/(Orbit/Maple woff2 ~20MB)。仅留品牌字标 Fredoka(logo Wordmark),Justfile ios-fonts 改 Fredoka-only、.gitignore 收窄、新增 Shared/Fonts/README.md
- iPad 适配:RootView TabView 加 .tabViewStyle(.sidebarAdaptable)(iPad 常规宽度侧边栏 / iPhone 底部标签栏)+ ConversationView 消息线程 maxWidth 680 居中;project.yml 补方向键 UISupportedInterfaceOrientations(iPhone 竖 + 横)/ ~ipad(全 4 向),消除 iPad 多任务方向构建告警
- 登录页内容约束 maxWidth 420 居中,避免 iPad 宽屏主登录按钮 / 二维码区横向拉满(iPhone 不受影响)
- 文档:ios/PLAN.md §11.2 改系统原生字体 + 新增 §13 iPad;ios/PARITY.md 字体分叉条目回退 + 导航条目加 iPad + 2026-07-07 漂移行
- 已装 iPhone 15 Pro C + iPad Pro 13" M4 双端、用户确认
This commit is contained in:
2026-07-07 20:49:06 +08:00
parent adfd2912a5
commit 7f6e35908c
18 changed files with 123 additions and 174 deletions
+3 -5
View File
@@ -10,11 +10,9 @@ DerivedData/
# 原生数据面引擎(曾是 gomobile/pion → xcframework;现已改 libwebrtc SPM,本目录废弃但留忽略防残留)。
Frameworks/
# 品牌字体不入库:Orbit Gothic 为专有 / 定制字体(公开仓库不应分发);Maple Mono / Fredoka 虽开源,
# 也统一不入库、改由 `just ios-fonts` 从 CDN / Google Fonts 拉取(镜像 web 的 CDN 思路),保持仓库精简
# 见 Sources/Fonts/README.md。构建前 `just ios-fonts`(已 chain 进 ios-device / ios-sim-build)会自动备齐
Sources/Fonts/*.woff2
Sources/Fonts/*.ttf
# 品牌字标字体 Fredokalogo / Wordmark 用)不入库:虽为开源 OFL,仍统一从 Google Fonts 拉、保持仓库
# 精简。见 Shared/Fonts/README.md。构建前 `just ios-fonts`(已 chain 进 ios-device / ios-sim-build)自动备齐
# UI 界面字体已改用系统原生字体,不再打包 Orbit Gothic / Maple Mono
Shared/Fonts/*.ttf
Shared/Fonts/*.woff2
+30
View File
@@ -0,0 +1,30 @@
# 品牌字标字体 Fredoka(不入库,构建前拉取)
iOS App 的 **UI 界面字体已统一用系统原生字体**`.font(.body)` / `.headline` / … 语义样式随 Dynamic Type
缩放;日志等宽处用 `.system(…, design: .monospaced)` 原生等宽字体)。**唯一保留的自定义字体是品牌字标
Fredoka**——仅用于 logo 的 Wordmark(「Commilitia Drop」,见 `Shared/Brand.swift`),与 web 品牌资产一致。
| 角色 | 字体 | 落地 | 来源 |
|---|---|---|---|
| 字标(Wordmark,仅 logo | **Fredoka SemiBold** | `Shared/Fonts/Fredoka-SemiBold.ttf` | Google Fonts 变量字体实例化(OFL |
## 为什么不提交进仓库
Fredoka 虽为开源(OFL),仍统一走拉取以保持仓库精简;经 `.gitignore` 排除,本 README 入库作说明。
## 如何备齐
```sh
just ios-fonts
```
幂等(已存在则跳过)。`just ios-device` / `just ios-sim-build` 构建前已自动 chain 本步,正常无需手动跑。
**前置**`curl`;造 Fredoka 静态实例需 `python3` + `fonttools``python3 -m pip install --user fonttools`)。
## 接入方式(代码)
Fredoka 是 ttf,经 `UIAppFonts`(各 target 的 Info.plist)注册;`Wordmark``Shared/Brand.swift`)用
`.font(.custom("Fredoka-SemiBold", size:))`。主 app、Share 扩展、控件扩展均可用(字标体积小,无 CJK)。
> 历史:早先曾为 UI 引入 Orbit GothicSans+ Maple MonoMono)自定义字体栈,后统一回退系统原生字体
> 移除(仅保留 Fredoka 字标)。故不再有 `Sources/Fonts/` 与运行期 `CTFontManagerRegisterFontsForURL` 注册。
-2
View File
@@ -10,8 +10,6 @@ final class AppDelegate: NSObject, UIApplicationDelegate
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool
{
// UI + / Orbit Gothic
BrandFonts.register()
// BGContinuedProcessingTask launch handler launch
BackgroundTaskManager.shared.registerOnce()
return true
+7 -3
View File
@@ -35,7 +35,7 @@ struct LoginView: View
if !auth.statusText.isEmpty
{
Text(auth.statusText)
.font(.oCallout)
.font(.callout)
.foregroundStyle(auth.qrExpired ? Color.red : Color.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
@@ -49,7 +49,7 @@ struct LoginView: View
qrArea
refreshButton
Text(t("ios.login.guide"))
.font(.oFootnote)
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
@@ -58,7 +58,7 @@ struct LoginView: View
label:
{
Text(t("ios.login.scanAlt"))
.font(.oSubheadline)
.font(.subheadline)
}
.padding(.horizontal, 40)
.onChange(of: showQR)
@@ -68,6 +68,10 @@ struct LoginView: View
Spacer()
}
// iPad /
//iPhone 420
.frame(maxWidth: 420)
.frame(maxWidth: .infinity)
.padding()
.tint(.cdropAccent)
}
-81
View File
@@ -1,81 +0,0 @@
import CoreText
import SwiftUI
import UIKit
// web --font-sans / --font-mono
// - Sans Orbit Gothic CJK SCRegular 400 + SemiBold 600
// - Mono Maple Mono /
// woff2 TTF 1/3iOS Core Text iOS 13 woff/woff2
// CTFontManagerRegisterFontsForURL UIAppFonts ttf/otfwoff2
// app CJK ~10MB/ Share / + Fredoka
// Orbit Gothic iOS 退
enum BrandFonts
{
static let sansRegular = "OrbitGothicCJKsc-Regular"
static let sansSemibold = "OrbitGothicCJKsc-SemiBold"
static let mono = "MapleMono-Regular"
// app AppDelegate didFinishLaunching + UIKit
static func register()
{
for name in [ sansRegular, sansSemibold, mono ]
{
guard let url = Bundle.main.url(forResource: name, withExtension: "woff2") else { continue }
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
}
applyUIKitAppearance()
}
// SwiftUI / UIKit SwiftUI API appearance Orbit
private static func applyUIKitAppearance()
{
if let large = UIFont(name: sansSemibold, size: 32)
{
UINavigationBar.appearance().largeTitleTextAttributes = [ .font: large ]
}
if let inline = UIFont(name: sansSemibold, size: 17)
{
UINavigationBar.appearance().titleTextAttributes = [ .font: inline ]
}
if let tab = UIFont(name: sansRegular, size: 10)
{
let item = UITabBarItem.appearance()
item.setTitleTextAttributes([ .font: tab ], for: .normal)
item.setTitleTextAttributes([ .font: tab ], for: .selected)
}
}
}
// Orbit GothicSans+ Maple MonoMonorelativeTo
// 访weight bold SemiBold Regular
extension Font
{
static func orbit(_ size: CGFloat, bold: Bool = false, relativeTo style: Font.TextStyle = .body) -> Font
{
.custom(bold ? BrandFonts.sansSemibold : BrandFonts.sansRegular, size: size, relativeTo: style)
}
static func maple(_ size: CGFloat, relativeTo style: Font.TextStyle = .body) -> Font
{
.custom(BrandFonts.mono, size: size, relativeTo: style)
}
// SF Orbit / SF .font(.xxx)
// headline / title* SemiBold / Regular
static let oLargeTitle = orbit(34, bold: true, relativeTo: .largeTitle)
static let oTitle = orbit(28, bold: true, relativeTo: .title)
static let oTitle2 = orbit(22, bold: true, relativeTo: .title2)
static let oTitle3 = orbit(20, bold: true, relativeTo: .title3)
static let oHeadline = orbit(17, bold: true, relativeTo: .headline)
static let oBody = orbit(17, relativeTo: .body)
static let oCallout = orbit(16, relativeTo: .callout)
static let oSubheadline = orbit(15, relativeTo: .subheadline)
static let oFootnote = orbit(13, relativeTo: .footnote)
static let oCaption = orbit(12, relativeTo: .caption)
static let oCaption2 = orbit(11, relativeTo: .caption2)
// Mono / id /
static let mCaption = maple(12, relativeTo: .caption)
static let mFootnote = maple(13, relativeTo: .footnote)
static let mBody = maple(15, relativeTo: .body)
}
-3
View File
@@ -15,9 +15,6 @@ struct CDropApp: App
WindowGroup
{
AppRoot(auth: auth, engine: engine)
// Orbit Gothic .font Sans
// .font(.oXxx) BrandFonts
.environment(\.font, .oBody)
}
.onChange(of: scenePhase)
{ _, phase in
-40
View File
@@ -1,40 +0,0 @@
# 品牌字体(不入库,构建前拉取)
iOS App 的字体栈对齐 web(见 `web/src/styles/fonts.css`):
| 角色 | 字体 | 落地 | 来源 |
|---|---|---|---|
| Sans(正文 / 界面) | **Orbit Gothic CJK SC**Regular 400 + SemiBold 600 | `Sources/Fonts/OrbitGothicCJKsc-*.woff2` | 私有 CDN `orbit-gothic.cdn.commilitia.net` |
| Mono(等宽) | **Maple Mono**Regular | `Sources/Fonts/MapleMono-Regular.woff2` | 私有 CDN `maple-mono.cdn.commilitia.net`OFL |
| 字标(Wordmark | **Fredoka SemiBold** | `Shared/Fonts/Fredoka-SemiBold.ttf` | Google Fonts 变量字体实例化(OFL |
## 为什么不提交进仓库
- **Orbit Gothic 是专有 / 定制字体**——公开仓库(见根 README 的双 remote 发布)不应分发它。web 端同样不入库、
改从私有 CDN 拉。
- Maple Mono / Fredoka 虽开源(OFL),也统一走拉取,保持仓库精简(Orbit 两变体 woff2 合计 ~20MB)。
字体文件经 `.gitignore` 排除(`Sources/Fonts/*.woff2,*.ttf``Shared/Fonts/*`);本 README 入库作说明。
## 如何备齐
```sh
just ios-fonts
```
幂等(已存在则跳过)。`just ios-device` / `just ios-sim-build` 构建前已自动 chain 本步,正常无需手动跑。
**前置**`curl`;造 Fredoka 静态实例需 `python3` + `fonttools``python3 -m pip install --user fonttools`)。
CDN 不可达时构建会失败——离线则手动把上述文件放到对应目录即可。
## 接入方式(代码)
字体以 **woff2** 内嵌(体积约 TTF 的 1/3iOS 13+ Core Text 原生支持 woff/woff2)。`UIAppFonts`Info.plist
历史上只认 ttf/otf,故 woff2 走**运行期注册** `CTFontManagerRegisterFontsForURL`——见 `Sources/BrandFonts.swift`
`BrandFonts.register()``AppDelegate` 启动调用)。Fredoka 字标是 ttf,经 `UIAppFonts` 注册。
仅**主 app** 接入 Orbit / MapleCJK 字体大,不进 Share / 控件扩展——它们保持系统字 + 仅 Fredoka 字标)。
生僻字超出 Orbit Gothic 字汇时由 iOS 字体级联自动回退系统苹方,不出豆腐块。
字体助手见 `Sources/BrandFonts.swift``Font.oBody/.oHeadline/...`Orbit,对位 SF 语义样式)、`Font.maple(...)`
Maple Mono);导航 / 标签栏标题经 UIKit appearance 代理换 Orbit。
+13
View File
@@ -70,5 +70,18 @@
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+2 -2
View File
@@ -109,10 +109,10 @@ struct LogView: View
HStack(spacing: 6)
{
Image(systemName: levelSymbol(entry.level))
.font(.oCaption)
.font(.caption)
.foregroundStyle(levelTint(entry.level))
Text(timeString(entry.at))
.font(.oCaption2)
.font(.caption2)
.foregroundStyle(.secondary)
.monospacedDigit()
}
+10 -7
View File
@@ -69,16 +69,16 @@ struct MessagesView: View
HStack(spacing: 12)
{
Image(systemName: deviceSymbol(convo.deviceType))
.font(.oTitle3)
.font(.title3)
.foregroundStyle(convo.online ? Color.green : Color.secondary)
.frame(width: 28)
VStack(alignment: .leading, spacing: 3)
{
Text(convo.peerName)
.font(.oBody)
.font(.body)
.lineLimit(1)
Text(previewText(convo))
.font(.oCaption)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
@@ -88,7 +88,7 @@ struct MessagesView: View
if convo.lastAt > 0
{
Text(relativeTime(convo.lastAt))
.font(.oCaption2)
.font(.caption2)
.foregroundStyle(.secondary)
}
// tab engine.unreadMessages
@@ -96,7 +96,7 @@ struct MessagesView: View
if unread > 0
{
Text("\(unread)")
.font(.oCaption2)
.font(.caption2)
.fontWeight(.bold)
.foregroundStyle(.white)
.padding(.horizontal, 7)
@@ -159,6 +159,9 @@ struct ConversationView: View
}
}
.padding()
// iPad iMessage
.frame(maxWidth: 680)
.frame(maxWidth: .infinity)
}
.onChange(of: thread.count) { scrollToLast(proxy) }
.onAppear { scrollToLast(proxy) }
@@ -213,7 +216,7 @@ struct ConversationView: View
.submitLabel(.send)
.onSubmit { send() }
Button { send() }
label: { Image(systemName: "paperplane.fill").font(.oTitle3) }
label: { Image(systemName: "paperplane.fill").font(.title3) }
.disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
.padding()
@@ -222,7 +225,7 @@ struct ConversationView: View
else
{
Text(t("ios.messages.viewOnly"))
.font(.oCaption)
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
.padding()
+2 -2
View File
@@ -35,10 +35,10 @@ private struct NoticeBanner: View
HStack(spacing: 10)
{
Image(systemName: symbol)
.font(.oHeadline)
.font(.headline)
.foregroundStyle(tint)
Text(notice.text)
.font(.oSubheadline)
.font(.subheadline)
.foregroundStyle(.primary)
.lineLimit(2)
Spacer(minLength: 8)
+13 -9
View File
@@ -60,6 +60,10 @@ struct RootView: View
}
}
}
// iPad iPadFiles / Music +
// iPhone TabView sidebarAdaptable iPad
// NavigationStack ios/PLAN.md §13
.tabViewStyle(.sidebarAdaptable)
.tabBarMinimizeBehavior(.onScrollDown)
.tint(.cdropAccent)
// #2
@@ -162,7 +166,7 @@ struct TransferListView: View
label:
{
Image(systemName: "paperplane.fill")
.font(.oTitle2)
.font(.title2)
.padding(18)
}
.buttonStyle(.glassProminent)
@@ -220,7 +224,7 @@ struct TransferListView: View
HStack
{
Text(title)
.font(.oSubheadline)
.font(.subheadline)
.foregroundStyle(.secondary)
Spacer()
}
@@ -342,7 +346,7 @@ struct TransferCardView: View
? "arrow.down.circle" : "arrow.up.circle")
.foregroundStyle(.secondary)
Text(item.fileName)
.font(.oHeadline)
.font(.headline)
.lineLimit(1)
Spacer()
}
@@ -351,7 +355,7 @@ struct TransferCardView: View
ProgressView(value: progressOf(item))
}
Text(detailLine)
.font(.oCaption)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
@@ -625,14 +629,14 @@ struct DeviceListView: View
if isSelf(dev)
{
Text(t("ios.devices.thisDevice"))
.font(.oCaption2)
.font(.caption2)
.foregroundStyle(Color.cdropAccent)
}
}
Text(dev.online
? t("settings.online")
: t("settings.lastSeen", [ "time": relativeTime(dev.lastSeen) ]))
.font(.oCaption)
.font(.caption)
.foregroundStyle(.secondary)
}
}
@@ -664,7 +668,7 @@ struct SettingsView: View
if !appVersion.isEmpty
{
Text(appVersion)
.font(.oCaption2)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
@@ -718,7 +722,7 @@ struct SettingsView: View
if !engine.lastEngineLog.isEmpty
{
Text(engine.lastEngineLog)
.font(.oCaption2)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
@@ -861,7 +865,7 @@ struct ReceivedFilesView: View
Text(url.lastPathComponent)
.lineLimit(1)
Text(fileSizeString(url))
.font(.oCaption)
.font(.caption)
.foregroundStyle(.secondary)
}
}
+2 -2
View File
@@ -59,14 +59,14 @@ struct ScanSheet: View
if invalid
{
Text(t("ios.scan.invalid"))
.font(.oCallout)
.font(.callout)
.padding(.vertical, 8)
.padding(.horizontal, 14)
.background(.red.opacity(0.85), in: Capsule())
.foregroundStyle(.white)
}
Text(t("ios.scan.hint"))
.font(.oFootnote)
.font(.footnote)
.multilineTextAlignment(.center)
.padding(.vertical, 8)
.padding(.horizontal, 14)
+1 -1
View File
@@ -40,7 +40,7 @@ struct SendComposeSheet: View
.lineLimit(1)
Spacer()
Text(formatBytes(file.size))
.font(.oCaption)
.font(.caption)
.foregroundStyle(.secondary)
}
}
+12
View File
@@ -57,6 +57,18 @@ targets:
- zh-Hant
- en
UILaunchScreen: {}
# 界面方向:iPhone 竖屏 + 横屏(不含上下颠倒,遵 iPhone 惯例);iPad 支持全部 4 向——通用 App
# 若不锁全屏(UIRequiresFullScreen 未设),iPad 多任务(Split View / Slide Over)要求支持全部方向,
# 否则构建告警「All interface orientations must be supported...」。
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
"UISupportedInterfaceOrientations~ipad":
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
# 后台续传:BGContinuedProcessingTask 的通配标识符须在此声明,系统才允许注册 / 提交
# (见 BackgroundTaskManager)。默认资源(CPU + 网络)无需额外 entitlement。
BGTaskSchedulerPermittedIdentifiers: