From 3d5ad8a614735d560dbc9c2caf791469dd712b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Thu, 16 Jul 2026 19:02:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BA=E5=AE=9A=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E5=B8=B8=E7=94=A8=E5=BA=94=E7=94=A8=E5=9B=BE=E6=A0=87=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Services/HomeMenuIconFactory.swift | 43 ++++++++++-- .../UI/Home/Views/AllFunctionMenuCell.swift | 5 +- .../UI/Home/Views/HomeCollectionCells.swift | 14 +++- suixinkanTests/HomeMenuIconFactoryTests.swift | 68 ++++++++++++++----- 4 files changed, 104 insertions(+), 26 deletions(-) diff --git a/suixinkan/Features/Home/Services/HomeMenuIconFactory.swift b/suixinkan/Features/Home/Services/HomeMenuIconFactory.swift index 3f07f02..3edf06a 100644 --- a/suixinkan/Features/Home/Services/HomeMenuIconFactory.swift +++ b/suixinkan/Features/Home/Services/HomeMenuIconFactory.swift @@ -8,14 +8,49 @@ import UIKit /// 首页 / 全部功能菜单图标加载,统一 SF Symbol 与资源图的视觉尺寸。 enum HomeMenuIconFactory { + /// 菜单图标统一使用的逻辑画布尺寸。 + static let canvasSize = CGSize(width: 24, height: 24) + /// 与菜单图标槽位(约 24–26pt)对齐的系统图标配置。 static let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium) - /// 优先加载 Assets 资源图;否则按统一配置加载 SF Symbol。 + private static let imageCache = NSCache() + + /// 优先加载 Assets 资源图;否则加载 SF Symbol,并统一渲染到固定画布。 static func image(named iconName: String) -> UIImage? { - if let asset = UIImage(named: iconName) { - return asset + let cacheKey = iconName as NSString + if let cachedImage = imageCache.object(forKey: cacheKey) { + return cachedImage } - return UIImage(systemName: iconName, withConfiguration: symbolConfiguration) + + guard let sourceImage = UIImage(named: iconName) + ?? UIImage(systemName: iconName, withConfiguration: symbolConfiguration) else { + return nil + } + + let normalizedImage = renderOnFixedCanvas(sourceImage) + imageCache.setObject(normalizedImage, forKey: cacheKey) + return normalizedImage + } + + private static func renderOnFixedCanvas(_ image: UIImage) -> UIImage { + let sourceSize = image.size + guard sourceSize.width > 0, sourceSize.height > 0 else { + return image + } + + let scale = min(canvasSize.width / sourceSize.width, canvasSize.height / sourceSize.height) + let drawSize = CGSize(width: sourceSize.width * scale, height: sourceSize.height * scale) + let drawRect = CGRect( + x: (canvasSize.width - drawSize.width) / 2, + y: (canvasSize.height - drawSize.height) / 2, + width: drawSize.width, + height: drawSize.height + ) + let renderer = UIGraphicsImageRenderer(size: canvasSize) + let renderedImage = renderer.image { _ in + image.withRenderingMode(.alwaysOriginal).draw(in: drawRect) + } + return renderedImage.withRenderingMode(.alwaysTemplate) } } diff --git a/suixinkan/UI/Home/Views/AllFunctionMenuCell.swift b/suixinkan/UI/Home/Views/AllFunctionMenuCell.swift index 307622e..b5e1bc1 100644 --- a/suixinkan/UI/Home/Views/AllFunctionMenuCell.swift +++ b/suixinkan/UI/Home/Views/AllFunctionMenuCell.swift @@ -75,9 +75,8 @@ final class AllFunctionMenuCell: UICollectionViewCell { cardView.clipsToBounds = true iconView.tintColor = AppColor.primary - iconView.contentMode = .scaleAspectFit - iconView.preferredSymbolConfiguration = HomeMenuIconFactory.symbolConfiguration - + iconView.contentMode = .center + iconView.adjustsImageSizeForAccessibilityContentSizeCategory = false contentStackView.axis = .vertical contentStackView.alignment = .center diff --git a/suixinkan/UI/Home/Views/HomeCollectionCells.swift b/suixinkan/UI/Home/Views/HomeCollectionCells.swift index f4a3898..0bf915d 100644 --- a/suixinkan/UI/Home/Views/HomeCollectionCells.swift +++ b/suixinkan/UI/Home/Views/HomeCollectionCells.swift @@ -127,6 +127,13 @@ final class HomeMenuCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + override func prepareForReuse() { + super.prepareForReuse() + iconView.image = nil + iconView.accessibilityIdentifier = nil + titleLabel.text = nil + } + func apply(menu: HomeMenuItem) { iconView.image = HomeMenuIconFactory.image(named: menu.iconName) iconView.accessibilityIdentifier = menu.iconName @@ -146,9 +153,8 @@ final class HomeMenuCell: UICollectionViewCell { iconContainerView.layer.cornerRadius = 20 iconView.tintColor = AppColor.primary - iconView.contentMode = .scaleAspectFit - iconView.preferredSymbolConfiguration = HomeMenuIconFactory.symbolConfiguration - + iconView.contentMode = .center + iconView.adjustsImageSizeForAccessibilityContentSizeCategory = false contentStackView.axis = .vertical contentStackView.alignment = .center @@ -176,6 +182,8 @@ final class HomeMenuCell: UICollectionViewCell { iconContainerView.snp.makeConstraints { make in make.width.height.equalTo(40) } + iconContainerView.setContentHuggingPriority(.required, for: .vertical) + iconContainerView.setContentCompressionResistancePriority(.required, for: .vertical) iconView.snp.makeConstraints { make in make.center.equalToSuperview() make.width.height.equalTo(24) diff --git a/suixinkanTests/HomeMenuIconFactoryTests.swift b/suixinkanTests/HomeMenuIconFactoryTests.swift index 44e7c02..0148110 100644 --- a/suixinkanTests/HomeMenuIconFactoryTests.swift +++ b/suixinkanTests/HomeMenuIconFactoryTests.swift @@ -14,27 +14,25 @@ final class HomeMenuIconFactoryTests: XCTestCase { let image = HomeMenuIconFactory.image(named: "home_menu_space") XCTAssertNotNil(image) + XCTAssertEqual(image?.size, HomeMenuIconFactory.canvasSize) XCTAssertNil(UIImage(systemName: "home_menu_space")) } - func testSystemSymbolMatchesSharedConfiguration() { - let expected = UIImage( - systemName: "ellipsis", - withConfiguration: HomeMenuIconFactory.symbolConfiguration - ) - let actual = HomeMenuIconFactory.image(named: "ellipsis") + func testSystemSymbolsUseFixedCanvas() { + let iconNames = [ + "ellipsis", + "dot.radiowaves.left.and.right", + "rectangle.stack.badge.plus", + ] - XCTAssertNotNil(actual) - XCTAssertEqual(actual?.size, expected?.size) - } + for iconName in iconNames { + let image = HomeMenuIconFactory.image(named: iconName) - func testSystemSymbolDiffersFromUnconfiguredDefault() { - let configured = HomeMenuIconFactory.image(named: "ellipsis") - let plain = UIImage(systemName: "ellipsis") - - XCTAssertNotNil(configured) - XCTAssertNotNil(plain) - XCTAssertNotEqual(configured?.size, plain?.size) + XCTAssertNotNil(image) + XCTAssertEqual(image?.size, HomeMenuIconFactory.canvasSize) + XCTAssertEqual(image?.renderingMode, .alwaysTemplate) + XCTAssertFalse(image?.isSymbolImage ?? true) + } } func testComplexSystemSymbolResolves() { @@ -42,3 +40,41 @@ final class HomeMenuIconFactoryTests: XCTestCase { XCTAssertNotNil(HomeMenuIconFactory.image(named: "rectangle.stack.badge.plus")) } } + +/// 首页常用应用 cell 的图标布局与复用测试。 +@MainActor +final class HomeMenuCellTests: XCTestCase { + + func testIconKeepsFixedSizeWhenReusedAtIPhoneXWidth() throws { + let cell = HomeMenuCell(frame: CGRect(x: 0, y: 0, width: 109, height: 109)) + let menus = [ + HomeMenuItem(uri: "space", title: "空间设置", iconName: "home_menu_space"), + HomeMenuItem(uri: "live", title: "直播管理", iconName: "dot.radiowaves.left.and.right"), + HomeMenuItem(uri: "more", title: "更多功能", iconName: "ellipsis"), + ] + + for menu in menus { + cell.prepareForReuse() + cell.apply(menu: menu) + cell.setNeedsLayout() + cell.layoutIfNeeded() + + let iconView = try XCTUnwrap(findImageView(in: cell, identifier: menu.iconName)) + XCTAssertEqual(iconView.bounds.size, CGSize(width: 24, height: 24)) + XCTAssertEqual(iconView.superview?.bounds.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(iconView.image?.size, HomeMenuIconFactory.canvasSize) + XCTAssertEqual(iconView.contentMode, .center) + XCTAssertNil(iconView.preferredSymbolConfiguration) + } + } + + private func findImageView(in view: UIView, identifier: String) -> UIImageView? { + if let imageView = view as? UIImageView, + imageView.accessibilityIdentifier == identifier { + return imageView + } + return view.subviews.lazy.compactMap { + self.findImageView(in: $0, identifier: identifier) + }.first + } +}