Files
Commilitia-Drop/ios/CDrop/Share/ShareViewController.swift
T
admin 349d94aa8f iOS 原生客户端:libwebrtc 数据面 + UX 改造 + 品牌/字体 + 全平台平均速度
- 原生数据面 libwebrtc(PLAN 决策 F):新增 Engine/LibWebRtcEngine.swift(RTCPeerConnection,端口 engine.go/session.go 线协议 + 水位背压 + ack 完成 + 不冲突落盘);NativeTransfer.swift 的 P2PEventsBridge 改 conform P2PEngineDelegate(去 gomobile);EngineController 4 个 p2p RPC 改调新引擎(completion-based + resolveOnMain)。stasel/WebRTC M149 经 SPM(exactVersion 149.0.0)。环回端到端 20MB 逐字节测过(Tests/)
- UX 改造(PARITY 残余项 1/2):新增 SendComposeSheet(发送预览=文件名 + 选设备,手动多选与分享共用);分享直入预览 + scenePhase .active 兜底(修“分享后必须重启 app”);重写 MessagesView(按设备会话 + 每设备未读红点 + 删单条 / 删整设备历史);EngineController 加 conversations / unread 模型
- 品牌 logo:新增 Shared/Brand.swift(BrandMark / Wordmark / BrandLockup / BrandHero)+ Brand.xcassets/LogoMark;登录页 / 传输空态 / 设置头 / 控制中心控件接 logo;Theme 加 cdropAmber
- 字体(对齐 web,仅主 app):新增 BrandFonts.swift(Orbit Gothic + Maple Mono woff2 运行期 CTFontManager 注册 + UIKit 外观代理 + .oXxx / .maple 助手);Fredoka SemiBold 字标;7 视图 .font sweep。字体不入库——.gitignore 排除 + just ios-fonts 构建前从 CDN / Google Fonts 拉取,见 Sources/Fonts/README.md
- 分享引导:新增 Share/ShareGuideView(吉祥物卡 + “打开 Commilitia Drop”);ShareViewController 健壮 staging(文件 / 数据双路)+ 响应链 IMP 唤起宿主
- 全平台平均速度:传输完成态显平均速度(总字节 / 总耗时)而非末次瞬时——web store/slices/transfer.ts + TransferRow.tsx + iOS RootView 卡片 / 详情;i18n 加 transfer.avgRate
- 工程:project.yml 接 SPM WebRTC + CDropTests + Fredoka UIAppFonts + Share i18n;Justfile 加 ios-sim-build + ios-fonts / ios-fonts-fredoka 配方
- 文档:ios/PLAN.md(决策 F + §10 UX + §11 品牌/字体 + §12 平均速度)、ios/PARITY.md(字体分叉条目更新 + 2026-06-28 漂移行)
2026-06-28 19:33:08 +08:00

175 lines
7.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import UIKit
import UniformTypeIdentifiers
// Share Extension share sheet App Group ****
// ShareGuideView + + + Commilitia Drop app
// 120MB jetsam PLAN §I5
//
// iOS share 宿 app
// openURL: iOS 10 iOS 18+
// IMP open App Group app
// CDropApp.scenePhase(.active)loadPendingShares
final class ShareViewController: UIViewController
{
private let model = ShareModel()
private var started = false
// URL
private var stagedURLs: [URL] = []
override func viewDidLoad()
{
super.viewDidLoad()
let host = UIHostingController(
rootView: ShareGuideView(
model: model,
onOpen: { [weak self] in self?.openAndFinish() },
onCancel: { [weak self] in self?.cancelAndFinish() }))
addChild(host)
host.view.frame = view.bounds
host.view.autoresizingMask = [ .flexibleWidth, .flexibleHeight ]
view.addSubview(host.view)
host.didMove(toParent: self)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
guard !started else { return } // viewDidAppear
started = true
Task { await stageAll() }
}
private func stageAll() async
{
let items = (extensionContext?.inputItems as? [NSExtensionItem]) ?? []
var urls: [URL] = []
for item in items
{
for provider in item.attachments ?? []
{
if let dest = await stage(provider) { urls.append(dest) }
}
}
stagedURLs = urls
await MainActor.run { model.fileCount = urls.count }
}
// Commilitia Drop app completeRequest
// completeRequest app
private func openAndFinish()
{
if let url = URL(string: "cdrop://share") { openViaResponder(url) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35)
{ [weak self] in
self?.extensionContext?.completeRequest(returningItems: nil)
}
}
// app app
private func cancelAndFinish()
{
for url in stagedURLs
{
try? FileManager.default.removeItem(at: url.deletingLastPathComponent())
}
extensionContext?.completeRequest(returningItems: nil)
}
// App Group URL退
// nil
private func stage(_ provider: NSItemProvider) async -> URL?
{
if let u = await stageFile(provider) { return u }
return await stageData(provider)
}
private func stageFile(_ provider: NSItemProvider) async -> URL?
{
let typeId = provider.registeredTypeIdentifiers.first ?? UTType.item.identifier
return await withCheckedContinuation
{ cont in
provider.loadFileRepresentation(forTypeIdentifier: typeId)
{ url, _ in
guard let url, let dest = AppGroup.stagedDestination(originalName: url.lastPathComponent)
else { cont.resume(returning: nil); return }
do
{
try FileManager.default.copyItem(at: url, to: dest)
cont.resume(returning: dest)
}
catch
{
cont.resume(returning: nil)
}
}
}
}
private func stageData(_ provider: NSItemProvider) async -> URL?
{
let typeId = provider.registeredTypeIdentifiers.first ?? UTType.data.identifier
let name = suggestedName(provider, typeId: typeId)
return await withCheckedContinuation
{ cont in
provider.loadDataRepresentation(forTypeIdentifier: typeId)
{ data, _ in
guard let data, let dest = AppGroup.stagedDestination(originalName: name)
else { cont.resume(returning: nil); return }
do
{
try data.write(to: dest)
cont.resume(returning: dest)
}
catch
{
cont.resume(returning: nil)
}
}
}
}
//
private func suggestedName(_ provider: NSItemProvider, typeId: String) -> String
{
if let s = provider.suggestedName, !s.isEmpty
{
if (s as NSString).pathExtension.isEmpty,
let ext = UTType(typeId)?.preferredFilenameExtension
{
return "\(s).\(ext)"
}
return s
}
let ext = UTType(typeId)?.preferredFilenameExtension ?? "dat"
return "shared-\(UUID().uuidString.prefix(8)).\(ext)"
}
// 沿 UIApplication IMP open(_:options:completionHandler:)
// UIApplication 退 openURL:iOS 18+
private func openViaResponder(_ url: URL)
{
let modern = NSSelectorFromString("openURL:options:completionHandler:")
var responder: UIResponder? = self
while let r = responder
{
if r.responds(to: modern), let app = r as? NSObject,
let method = class_getInstanceMethod(type(of: app), modern)
{
typealias OpenFn = @convention(c) (NSObject, Selector, NSURL, NSDictionary, Any?) -> Void
let fn = unsafeBitCast(method_getImplementation(method), to: OpenFn.self)
fn(app, modern, url as NSURL, NSDictionary(), nil)
return
}
responder = r.next
}
let legacy = NSSelectorFromString("openURL:")
responder = self
while let r = responder
{
if r.responds(to: legacy) { _ = r.perform(legacy, with: url); return }
responder = r.next
}
}
}