Align iOS home common apps and all-functions page with Android.

Add unified HomeCommonMenu/HomeAllFunctions diagnostics, rebuild all-functions from top-level menuList permissions, and document Home and AllFunctions module logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 09:40:02 +08:00
parent 63fb0462d6
commit 5692134efc
11 changed files with 693 additions and 138 deletions

View File

@ -0,0 +1,57 @@
//
// HomeAllFunctionsBuilder.swift
// suixinkan
//
// Created by Codex on 2026/6/30.
//
import Foundation
/// URI
struct HomeAllFunctionsSnapshot: Equatable {
let allFunctions: [HomeMenuItem]
let commonFunctions: [HomeMenuItem]
let moreFunctions: [HomeMenuItem]
}
/// Android `AllFunctionsViewModel`
enum HomeAllFunctionsBuilder {
/// URI /
static func build(
topLevelPermissions: [PermissionItem],
commonURIs: [String]
) -> HomeAllFunctionsSnapshot {
let allFunctions = allMenuItems(from: topLevelPermissions)
let commonSet = Set(commonURIs)
let commonFunctions = allFunctions.filter { commonSet.contains($0.uri) }
let moreFunctions = allFunctions.filter { !commonSet.contains($0.uri) }
HomeAllFunctionsDiagnostics.log(snapshot: HomeAllFunctionsSnapshot(
allFunctions: allFunctions,
commonFunctions: commonFunctions,
moreFunctions: moreFunctions
))
return HomeAllFunctionsSnapshot(
allFunctions: allFunctions,
commonFunctions: commonFunctions,
moreFunctions: moreFunctions
)
}
/// menuList API
private static func allMenuItems(from topLevelPermissions: [PermissionItem]) -> [HomeMenuItem] {
topLevelPermissions.compactMap { item -> HomeMenuItem? in
let uri = item.uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard !uri.isEmpty, HomeCommonMenuStore.androidHomeMenuURIs.contains(uri) else {
return nil
}
let fallbackTitle = item.name.isEmpty ? HomeMenuRouter.title(for: uri) : item.name
return HomeMenuItem(
title: HomeMenuRouter.displayTitle(for: uri, fallback: fallbackTitle),
uri: uri,
iconSrc: item.iconSrc
)
}
}
}

View File

@ -0,0 +1,29 @@
//
// HomeAllFunctionsDiagnostics.swift
// suixinkan
//
// Created by Codex on 2026/6/30.
//
import Foundation
import os
/// 便 Android `AllFunctionsViewModel`
enum HomeAllFunctionsDiagnostics {
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeAllFunctions")
/// URI
static func log(snapshot: HomeAllFunctionsSnapshot) {
#if DEBUG
log(step: "allFunctions", uris: snapshot.allFunctions.map(\.uri))
log(step: "commonFunctions", uris: snapshot.commonFunctions.map(\.uri))
log(step: "moreFunctions", uris: snapshot.moreFunctions.map(\.uri))
#endif
}
/// URI
private static func log(step: String, uris: [String]) {
let payload = "count=\(uris.count) uris=\(HomeCommonMenuDiagnostics.formatURIList(uris))"
logger.debug("step=\(step, privacy: .public) \(payload, privacy: .public)")
}
}

View File

@ -0,0 +1,33 @@
//
// HomeCommonMenuDiagnostics.swift
// suixinkan
//
// Created by Codex on 2026/6/30.
//
import Foundation
import os
/// Android `HomeCommonMenu` tag
enum HomeCommonMenuDiagnostics {
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeCommonMenu")
/// 线
static func log(step: String, fields: [String: String]) {
#if DEBUG
let payload = fields.map { "\($0.key)=\($0.value)" }.joined(separator: " ")
logger.debug("step=\(step, privacy: .public) \(payload, privacy: .public)")
#endif
}
/// URI
static func formatURIList(_ uris: [String]) -> String {
"[\(uris.joined(separator: ","))]"
}
/// title|uri
static func formatDisplayItems(_ items: [(title: String, uri: String)]) -> String {
let labels = items.map { "\($0.title)|\($0.uri)" }
return "[\(labels.joined(separator: ","))]"
}
}

View File

@ -49,25 +49,26 @@ struct HomeCommonMenuStore {
self.defaults = defaults
}
/// Android URI 4 4
static func defaultCommonURIs(
orderedTopLevelURIs: [String],
availableURIs: Set<String>
) -> [String] {
let resolved = orderedTopLevelURIs.compactMap { uri -> String? in
/// Android URI 4 URI menuList
static func defaultCommonURIs(orderedTopLevelURIs: [String]) -> [String] {
let rawTopLevel = orderedTopLevelURIs.compactMap { uri -> String? in
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let canonical = HomeMenuRouter.canonicalURI(for: trimmed, availableURIs: availableURIs)
return Self.isAndroidHomeMenuURI(canonical) && availableURIs.contains(canonical) ? canonical : nil
return trimmed.isEmpty ? nil : trimmed
}
let deduplicated = unique(resolved)
if deduplicated.count > 4 {
return Array(deduplicated.prefix(4))
}
return deduplicated
let candidates = rawTopLevel.count > 4 ? Array(rawTopLevel.prefix(4)) : rawTopLevel
return candidates.filter { isAndroidHomeMenuURI($0) }
}
/// URI Android
/// Android menuList URI API
static func availableTopLevelMenuURIs(from orderedTopLevelURIs: [String]) -> [String] {
orderedTopLevelURIs.compactMap { uri -> String? in
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, isAndroidHomeMenuURI(trimmed) else { return nil }
return trimmed
}
}
/// URI saved 退
func load(
menuItems: [HomeMenuItem],
roleCode: String?,
@ -78,55 +79,99 @@ struct HomeCommonMenuStore {
guard let roleCode, !roleCode.isEmpty else { return [] }
migrateLegacyStorageIfNeeded(roleCode: roleCode, accountScope: accountScope, legacyRoleId: legacyRoleId)
let availableURIs = Set(menuItems.map(\.uri))
let rawTopLevel = topLevelPermissionURIs.compactMap { uri -> String? in
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
let availableTopLevel = Self.availableTopLevelMenuURIs(from: rawTopLevel)
let availableTopLevelSet = Set(availableTopLevel)
let key = Self.storageKey(roleCode: roleCode, accountScope: accountScope)
let saved = defaults.stringArray(forKey: key)
let saved = defaults.stringArray(forKey: key) ?? []
let defaultCommon = Self.defaultCommonURIs(orderedTopLevelURIs: rawTopLevel)
if let saved, !saved.isEmpty {
let filtered = Self.unique(saved.compactMap { uri -> String? in
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
return Self.isAndroidHomeMenuURI(resolved) && availableURIs.contains(resolved) ? resolved : nil
})
if !filtered.isEmpty {
save(filtered, roleCode: roleCode, accountScope: accountScope)
return filtered
}
HomeCommonMenuDiagnostics.log(
step: "context",
fields: [
"roleCode": roleCode,
"accountScope": accountScope ?? "",
"storageKey": key
]
)
HomeCommonMenuDiagnostics.log(
step: "rawTopLevel",
fields: [
"count": String(rawTopLevel.count),
"uris": HomeCommonMenuDiagnostics.formatURIList(rawTopLevel)
]
)
HomeCommonMenuDiagnostics.log(
step: "availableFunctions",
fields: [
"count": String(availableTopLevel.count),
"uris": HomeCommonMenuDiagnostics.formatURIList(availableTopLevel)
]
)
HomeCommonMenuDiagnostics.log(
step: "savedCommon",
fields: [
"uris": HomeCommonMenuDiagnostics.formatURIList(saved)
]
)
HomeCommonMenuDiagnostics.log(
step: "defaultCommon",
fields: [
"uris": HomeCommonMenuDiagnostics.formatURIList(defaultCommon)
]
)
let finalCommon: [String]
if !saved.isEmpty {
finalCommon = saved.filter { availableTopLevelSet.contains($0) }
} else {
finalCommon = defaultCommon
save(finalCommon, roleCode: roleCode, accountScope: accountScope)
}
let defaultURIs = Self.defaultCommonURIs(
orderedTopLevelURIs: topLevelPermissionURIs,
availableURIs: availableURIs
HomeCommonMenuDiagnostics.log(
step: "finalCommon",
fields: [
"count": String(finalCommon.count),
"uris": HomeCommonMenuDiagnostics.formatURIList(finalCommon)
]
)
save(defaultURIs, roleCode: roleCode, accountScope: accountScope)
return defaultURIs
_ = menuItems
return finalCommon
}
/// URI
func add(_ uri: String, current: [String], menuItems: [HomeMenuItem], roleCode: String?, accountScope: String? = nil) -> [String] {
/// URI 使 URI
func add(
_ uri: String,
current: [String],
topLevelPermissionURIs: [String],
roleCode: String?,
accountScope: String? = nil
) -> [String] {
guard let roleCode, !roleCode.isEmpty else { return current }
let availableURIs = Set(menuItems.map(\.uri))
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
guard Self.isAndroidHomeMenuURI(resolved), availableURIs.contains(resolved) else { return current }
guard !current.contains(where: { HomeMenuRouter.menuAliasKey(for: $0) == HomeMenuRouter.menuAliasKey(for: resolved) }) else {
return current
}
let next = current + [resolved]
let availableTopLevel = Set(Self.availableTopLevelMenuURIs(from: topLevelPermissionURIs))
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard availableTopLevel.contains(trimmed), !current.contains(trimmed) else { return current }
let next = current + [trimmed]
save(next, roleCode: roleCode, accountScope: accountScope)
return next
}
/// URI URI
/// URI使 URI
func remove(_ uri: String, current: [String], roleCode: String?, accountScope: String? = nil) -> [String] {
guard let roleCode, !roleCode.isEmpty else { return current }
let aliasKey = HomeMenuRouter.menuAliasKey(for: uri)
let next = current.filter { HomeMenuRouter.menuAliasKey(for: $0) != aliasKey }
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
let next = current.filter { $0 != trimmed }
save(next, roleCode: roleCode, accountScope: accountScope)
return next
}
/// URI
func save(_ uris: [String], roleCode: String, accountScope: String? = nil) {
defaults.set(Self.unique(uris), forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
defaults.set(uris, forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
}
/// key roleCode legacy role id key
@ -170,12 +215,4 @@ struct HomeCommonMenuStore {
private static func isAndroidHomeMenuURI(_ uri: String) -> Bool {
androidHomeMenuURIs.contains(uri)
}
/// URI URI
private static func unique(_ uris: [String]) -> [String] {
var seen = Set<String>()
return uris.filter { uri in
seen.insert(HomeMenuRouter.menuAliasKey(for: uri)).inserted
}
}
}