SwiftUI 有一个专用的 Image
类型来处理应用程序中的图片,您可以通过以下主要方式创建它们:
Image(systemName: "cloud.heavyrain")
// 因为 SF 与 San Francisco 无缝整合。可以用 font 修饰符进行缩放
Image(systemName: "cloud.heavyrain").font(.system(size: 100))
// 因为 SF 是字体,所以可以应用常规修饰符,例如用 foregroundColor 来变更外观
Image(systemName: "cloud.heavyrain").font(.system(size: 100)).foregroundColor(.blue)
// 一些 SF 符号带有多种变体,您可以使用 symbolVariant 修饰符轻松调整。
// 例如,渲染一个带有斜线的钟形图标
Image(systemName: "bell").symbolVariant(.slash)
// 例如,渲染一个正方形围绕钟形
Image(systemName: "bell").symbolVariant(.square)
// 或者斜杠渲染“响铃”符号并填充响铃
Image(systemName: "bell").symbolVariant(.fill.slash)
// 某些 SF 符号支持可变着色,这意味着可以根据 0 到 1 之间的分数填充不同的部分。例如以下显示一个部分填充的 Wi-Fi 图标:
Image(systemName: "wifi", variableValue: 0.5)
//如果您使用的图像具有颜色元素,则可以使用.renderingMode(.original) 激活多色模式,如下所示:
Image(systemName: "cloud.sun.rain.fill")
.renderingMode(.original)
.font(.largeTitle)
.padding()
.background(.red)
.clipShape(Circle())
// 还可以将 foregroundStyle() 应用于多色 SF 符号,这将导致符号的一部分重新着色。例如,这将使图标的一部分呈现蓝色,一部分呈现绿色:
Image(systemName: "person.crop.circle.fill.badge.plus")
.renderingMode(.original)
.foregroundStyle(.blue)
.background(.red)
.clipShape(Circle())
// 分层渲染通过 .symbolRenderingMode 修饰符,使用不透明度来创建屏幕上阴影的变化
// 例如这将以透明方式绘制图像以提供额外的深度和清晰度:
Image(systemName: "theatermasks")
.symbolRenderingMode(.hierarchical)
.font(.system(size: 144))
// 分层渲染可以与前景色结合使用,因此如果需要,您可以指定两者:
Image(systemName: "theatermasks")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
// 还可以使用 .palette 变体来完全控制图像中的颜色
Image(systemName: "shareplay")
.symbolRenderingMode(.palette)
// 例如可以同时渲染 SharePlay 图标为蓝色和黑色,如下所示:
.foregroundStyle(.blue, .black)
// 这些颜色的应用方式取决于每个单独的符号是两层还是三层定义,如果是三种变体的符号,只需添加额外的颜色:
.foregroundStyle(.blue, .green, .red)
// 甚至适用于复杂的前景样式,例如为图标中的每个人提供一个渐变:
Image(systemName: "person.3.sequence.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(
.linearGradient(colors: [.red, .black], startPoint: .top, endPoint: .bottomTrailing),
.linearGradient(colors: [.green, .black], startPoint: .top, endPoint: .bottomTrailing),
.linearGradient(colors: [.blue, .black], startPoint: .top, endPoint: .bottomTrailing)
)
使用图片前首先要将图片导入到素材目录,也就是 Assets.xcassets
// 只需要指定圖片名稱,你就會在預覽畫布中見到圖片
Image("paris")
// 当使用这样的固定图像名称时,Xcode 会自动生成一个常量名称,因此可以使用这些名称来代替字符串(Xcode 15 及更高版本)
// 也就是写成 Image(.example) ,这比使用字符串安全,会有代码提示
Image(.paris)
// 由于使用 named 初始值设定项加载 UIImage 会返回一个可选图像,因此您应该添加默认值,
// 或者如果您确定它存在于资源目录中,则应使用强制展开:
Image(uiImage: UIImage(named: "cat")!)
// Image(decorative: "pencil") 同样可以加载图像,但不会为启用屏幕阅读器(screen reader)的用户读出该图像
// 这对于不传达其他重要信息的图像很有用
Image(decorative: "pencil")
SwiftUI 的 Image
视图非常适合应用程序包中的图像,但如果想从互联网加载远程图像,则需要使用 AsyncImage
。这些是使用图像 URL 而不是简单的资源名称或 Xcode 生成的常量创建的,但 SwiftUI 会为我们处理剩下的所有事情 – 它下载图像、缓存下载并自动显示它。
AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>"))
// 还可以提前告诉 SwiftUI 正在尝试加载 3 倍比例的图像,这个能大概设置尺寸,但不精确
AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>"), scale: 3)
// 请注意 URL 是可选值,如果 URL 字符串无效,AsyncImage 将仅显示默认的灰色占位符。
// 如果由于某种原因无法加载图像(如果用户离线,或者图像不存在),那么系统将继续显示相同的占位符图像。
AsyncImage
最大的问题是,传统的设置图像的修饰符,对它无效。因为 SwiftUI 在实际获取图像数据之前不知道如何应用它们。
AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>"))
// 这行加了无效
// 如果这样设置 frame ,实际上它只对包含图像的容器有效(应用程序运行时可以短暂看到占位符,200x200灰色方块,一旦加载完成就会自动消失)
.frame(width: 200, height: 200)
//这行加了程序崩溃
.resizable()
因此 AsyncImage
的正确写法是:
// 该写法意思是:一旦下载好图片,就会向我们传递最终的图像,然后就可以根据需要进行自定义。另外还提供了第二个闭包,可以自定义占位符
AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>")) {
image in
// 指代下载完成后的图像
image
.resizable()
.scaledToFit()
} placeholder: {
// 指代占位符:占位符视图可以是任何内容。例如替换为 ProgressView() ,那么将得到一个小旋转器活动指示器
Color.red
}
.frame(width: 200, height: 200)
还可以分阶段(下载时,下载完成,出错)对网络图像进行定义,当您想要在各个阶段显示不同的视图时,特别有用。
// 设置方式1:
AsyncImage(url: URL(string: "<https://hws.dev/img/bad.png>")) {
phase in
//下载完成时展示
if let image = phase.image {
image
.resizable()
.scaledToFit()
}
//下载出错时展示
else if phase.error != nil {
Text("There was an error loading the image.")
}
//下载过程中展示
else {
ProgressView()
}
}
.frame(width: 200, height: 200)
// 设置方式2: 目前有以下几种内置的阶段:.empty 代表加载尚未完成, .failure 代表图像加载失败, success 代表图像已准备好(如果它有效)
AsyncImage(url: URL(string: "<https://hws.dev/paul3.jpg>")) { phase in
switch phase {
case .failure:
Image(systemName: "photo")
.font(.largeTitle)
// let image 是枚举的关联值解包
case .success(let image):
image
.resizable()
default:
ProgressView()
}
}
.frame(width: 256, height: 256)
.clipShape(.rect(cornerRadius: 25))
修饰符示例 | 说明 | |
---|---|---|
设置 SF Symbol 图片大小 | .font(.system(size: 20)) | |
设置成可缩放 | .resizable() | 自动调整大小,以便填充所有可用空间( frame 中的或是屏幕上的) |
设置成平铺 | .resizable(resizingMode: .tile) | 可以添加 capInsets 参数设置缩进 |
保持比例显示 | .aspectRatio(contentMode: .fit) | fit、fill |
缩放模式 Fit | .scaledToFit() | 缩放以显示全部图片 |
缩放模式 Fill | .scaleToFill() | 缩放以填满全部 frame 区域 |
裁切框架外的图像内容 | .clipped() | |
设置裁切的形状 | .clipShape(.capsule) | capsule、ellipse、capsule、自定义形状 |
叠加视图 | .overlay(Image(systemName: "heart.fill")) | |
给图像上色 | .renderingMode(.template) | original 保留图像原始颜色,template 丢弃颜色信息仅绘制图像形状。使用后者就可以人为给它上色 |
饱和度 | .saturation(amount) | 指定一个介于 0(无颜色,仅灰度)和 1(全色)之间的值 |
混合模式 | .blendMode(.hardLight) | |
色相修改器 | .hueRotation(Angle(degrees: 30.0)) | |
设置图像插值 | .interpolation(.none) | 设置为none的时候可以实现像素效果 |
图片在未设置之前,默认尺寸一般就是原图的尺寸。预设图片使用的是延伸(stretch)模式,原始图片会被放大到填满整个屏幕,除了安全区域。也就是说,这个图片填满了整个 iOS 所定义的安全区域(saft area)。
这时如果给图像添加 Frame 修改器,不会直接拉伸图片,它只是定义图片的容器大小,图片默认会在里面居中。这个特别适合用于规范SF图标的大小统一。具体设置如下:
// 1. 首先设置 frame 修饰符,它给图片设置了显示区域,但区域外的内容还是会被显示,所以看起来没有变化
Image(.example)
.frame(width: 300, height: 300)
// 2. 加上clip裁切就会更容易发现这一点。加上clip后,frame区域外的内容就不显示了。
Image(.example)
.frame(width: 300, height: 300)
.clipped()
以上方式是裁切遮罩的方式,一般不是设置图片尺寸想要的。正确的设置方法是:
// 1. 先添加 resizable 属性,让图片可缩放
Image(.example)
.resizable()
.frame(width: 300, height: 300)
// 现在图片可以缩放了,但是比例被强行改变了,用于填满整个 frame。这不是想要的
Image(.example)
.resizable()
// 2. 这时加上以下修饰器(要保持图片的比例,可以用以下两种修饰器的其中1种)
.scaledToFit() // 显示完整图片,但框架内可能会未填满
.scaledToFill() // 全部填满区域,可能会牺牲一些图片内容
.frame(width: 300, height: 300)
.clipped() // 使用了 fill 的话,要用clip将区域外部的内容隐藏
另外 resizable
还可以设置图片平铺:
// 如果 SwiftUI 被要求使图像视图占用的空间超过图像设计的空间,它的默认行为是拉伸图像,使其适合您所要求的空间
// 然而,它也可以平铺图像,即水平和垂直重复它,以便完全填充空间
// 关键是使用 resizable() 修饰符及其 resizingMode 参数(可以是.stretch (默认值)或.tile)
Image("logo")
.resizable(resizingMode: .tile)
// 如果只想平铺图像的一部分(将一个或多个边缘固定到图像视图的边缘),可以为第一个参数提供边缘插入,如下所示:
Image("logo")
.resizable(capInsets: EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), resizingMode: .tile)
<aside> 💡 frame 修饰符不能放在 fill、stroke、trim 修饰符的前面,否则会报错!
</aside>
如果创建一个 Image
视图,将其内容拉伸到大于其原始大小,默认情况下 iOS 会进行图像插值(也就是平滑地混合像素)。当然,这会带来性能成本,但大多数时候并不值得担心。然而,图像插值在一个地方会导致问题,那就是当您希望处理精确像素时。仔细观察颜色的边缘,它们看起来参差不齐,但也很模糊。锯齿状部分来自原始图像,模糊部分是 SwiftUI 在拉伸时尝试混合像素以使拉伸不那么明显的地方。
对于像这样的情况,SwiftUI 提供了 interpolation()
修饰符,让我们可以控制像素混合的应用方式。这有多个级别,但实际上我们只关心一个: .none
。这会完全关闭图像插值,因此它们不会混合像素,而是会通过锐边进行放大。因此,将您的图像修改为:
Image(.example)
.interpolation(.none)
.resizable()
.scaledToFit()
.background(.black)