Files
Qiu Yuzhou fb428f12bc refactor(app): 按分层重组 App/ 源码目录
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。

按「目录即分层」重组为 Composition / Application / Presentation / UI /
PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、
Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、
StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有
散落文件。

配套改动:

- project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。
  XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后
  旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与
  entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何
  非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。
- SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免
  前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时
  适配器放进 Application/ 即自动落在扫描范围外,无需再登记。
- README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。

验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫);
task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型
warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位
后签名与 entitlements 语义不变。

注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist
会被当作普通资源扫进 Resources 阶段。
2026-10-05 19:56:18 +08:00

92 lines
3.7 KiB
Swift

import AppKit
import SwiftUI
/// 关掉系统给密码输入框的「密码自动填充」入口。
///
/// SwiftUI 的 `SecureField` 在 macOS 上由 `NSSecureTextField` 承载;系统对安全
/// 输入框默认提供钥匙串/密码 App 的自动填充入口。`textContentType(nil)` 只清掉
/// 字段语义(实测底层字段 `contentType` 已是 nil),关不掉入口;AppKit 没有公开
/// 开关,唯一开关是字段级的私有 `_setPasswordAutofillEnabled:`(默认开)。这里在
/// 探针认领到的文本框上按需调用它:方法不存在(系统换实现)时保持系统默认
/// 行为,不做替代实现,也不让 KVC 抛异常。
///
/// 用法:贴在密码控件(`Group { TextField / SecureField }`)的 `.background(...)`
/// 上。探针与控件同帧,据此在祖先里认领同帧的文本框,因此同一表单里的其他
/// 输入框不会被误伤。
struct PasswordAutofillOptOut: NSViewRepresentable {
func makeNSView(context: Context) -> ProbeView {
ProbeView()
}
func updateNSView(_ view: ProbeView, context: Context) {
view.disablePasswordAutofill()
}
/// 背景探针:由 `.background` 撑满密码控件,据此认领承载控件的文本框。
final class ProbeView: NSView {
/// 已处理过的文本框:明文/密文切换会换成新实例,换实例即重新处理。
private weak var claimedField: NSTextField?
private static let setterSelector = NSSelectorFromString("_setPasswordAutofillEnabled:")
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
disablePasswordAutofill()
}
override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize)
disablePasswordAutofill()
}
override func setFrameOrigin(_ newOrigin: NSPoint) {
super.setFrameOrigin(newOrigin)
disablePasswordAutofill()
}
func disablePasswordAutofill() {
let target = convert(bounds, to: nil)
guard window != nil, !target.isEmpty else { return }
guard let field = Self.field(overlapping: target, from: superview) else { return }
guard field !== claimedField else { return }
claimedField = field
guard field.responds(to: Self.setterSelector) else { return }
field.setValue(false, forKey: "passwordAutofillEnabled")
}
/// 逐层向上找第一个含候选文本框的祖先视图。按交叠面积认领而不是逐边
/// 相等:探针与控件同帧,取整差异不应该让认领落空。
private static func field(overlapping target: NSRect, from view: NSView?) -> NSTextField? {
var ancestor = view
while let current = ancestor {
if let match = bestMatch(in: current, target: target) { return match.field }
ancestor = current.superview
}
return nil
}
private static func bestMatch(
in view: NSView, target: NSRect
) -> (field: NSTextField, area: CGFloat)? {
var best: (field: NSTextField, area: CGFloat)?
if let field = view as? NSTextField, field.window != nil,
let area = intersectionArea(of: field, with: target), area > 0
{
best = (field, area)
}
for subview in view.subviews {
guard let candidate = bestMatch(in: subview, target: target) else { continue }
if candidate.area > (best?.area ?? 0) { best = candidate }
}
return best
}
private static func intersectionArea(of field: NSTextField, with target: NSRect) -> CGFloat? {
let rect = field.convert(field.bounds, to: nil)
let intersection = rect.intersection(target)
guard !intersection.isNull else { return nil }
return intersection.width * intersection.height
}
}
}