PAYATHON 2026

SwiftUI 按钮图标如何在工具栏和溢出菜单中都正确显示?

数据小陈

建议保留包含文字和图标的 Label,先使用工具栏的默认标签样式。如果自定义图片仍以原始尺寸显示,再把图片预先绘制成目标尺寸的 UIImage,交给 Label 使用。

原问题没有提供 iOS 版本和完整的工具栏声明,无法确认具体触发条件,也不能保证只增加某个 modifier 就能解决所有环境中的问题。

为什么两种写法表现不同?

第一种写法只有 Image,没有向系统提供按钮的文字标题。第二种写法用 Label 同时提供标题和图标,系统可以根据工具栏或菜单的显示环境使用这些信息。Apple 的 Label 文档介绍了这种标题与图标的组合方式。

.resizable()、.scaledToFit() 和 .frame(...) 调整的是 SwiftUI 视图的布局和绘制方式,不会生成尺寸更小的图片资源。

根据题目描述的现象,可以怀疑工具栏处理 Label 图标时没有沿用这些视图修饰符,而是使用了底层图片。这只是一个排查方向,现有信息还不足以认定它是某个版本的框架缺陷。

先使用默认的 Label 样式

先移除显式设置的 .labelStyle(.iconOnly),保留标题和图标,观察系统在工具栏和溢出菜单中的默认表现:

Button {
    // action
} label: {
    Label {
        Text("Button label")
    } icon: {
        Image(imageName)
            .resizable()
            .scaledToFit()
            .frame(
                width: toolbarButtonImageSize,
                height: toolbarButtonImageSize
            )
    }
}

这样保留了菜单需要的文字信息,也避免在按钮上额外强制标签样式。如果外层视图设置了 .labelStyle(...),也要检查它的影响。

这一步用于验证样式是否参与了问题。如果工具栏仍显示原始尺寸的图标,再处理图片本身。

将自定义图片预先绘制到目标尺寸

对于 iOS 上通过 Image(imageName) 加载的普通图片资源,可以先生成目标尺寸的 UIImage。这样,即使工具栏直接使用底层图片,拿到的也是已经缩小的图像。

下面的函数保持宽高比,将图片的最长边调整到 maxSide。maxSide 的单位是 point:

import SwiftUI
import UIKit

func resizedToolbarIcon(
    _ image: UIImage,
    maxSide: CGFloat
) -> UIImage {
    guard maxSide > 0,
          image.size.width > 0,
          image.size.height > 0 else {
        return image
    }

    let ratio = maxSide / max(
        image.size.width,
        image.size.height
    )

    let outputSize = CGSize(
        width: image.size.width * ratio,
        height: image.size.height * ratio
    )

    let renderer = UIGraphicsImageRenderer(size: outputSize)

    return renderer.image { _ in
        image.draw(in: CGRect(origin: .zero, size: outputSize))
    }
}

UIGraphicsImageRenderer 可以按指定尺寸创建图像,image(actions:) 返回绘制后的 UIImage。这部分 API 行为见 Apple 文档。

加载资源时生成并保存处理后的图片:

let toolbarIcon = UIImage(named: imageName).map {
    resizedToolbarIcon(
        $0,
        maxSide: toolbarButtonImageSize
    )
}

然后在原来的 ToolbarItem 中使用它:

if let toolbarIcon = toolbarIcon {
    Button {
        // action
    } label: {
        Label {
            Text("Button label")
        } icon: {
            Image(uiImage: toolbarIcon)
                .renderingMode(.template)
        }
    }
}

这里不再依赖 .resizable() 和 .frame(...) 缩小图标。示例中的 .template 适合随系统着色的单色图标;如果要保留图片原有颜色,可以改为 .original。

实际项目中应缓存生成的图片,避免每次 body 更新时重新绘制。资源加载失败时,上面的条件分支不会生成按钮,需要检查图片名称和资源所属的 bundle。

验证与注意事项

分别检查横屏和竖屏时的工具栏,以及展开后的溢出菜单,确认图标大小、菜单文字和按钮动作是否符合预期。

预先绘制可以控制传入图片的尺寸,但工具栏和菜单仍可能各自施加布局规则,因此不能保证两处图标的视觉尺寸完全相同。图片资源中的透明留白也会影响图标看起来的大小。

如果问题仍然存在,需要补充 iOS 版本、ToolbarItem 或 ToolbarItemGroup 的完整声明、placement,以及图片资源尺寸。若必须分别控制工具栏和菜单的展示,可以考虑显式创建 Menu,但这会把菜单内容及哪些操作进入菜单的管理交给应用,不会自动复现系统的溢出行为。

备注:内容仅供参考。