Text 文本视图

// 基本调用方法:Text 后面跟的参数必须是 String 类型
Text("Stay Hungry. Stay Foolish.")

// 字符串插值:如果要展示的信息不是 String 类型,需要用 \() 进行插值
Text("\(price)")

// Text也可以用于展示 Image:
Text(Image(systemName: "person"))
	.font(.system(.largeTitle, weight: .bold))
	
	
// Text还可以直接构造字符串数组,例如 stringArray 是一个数组[A,B,C] ,那可以直接这样构造
// 最终展示为 “A, B, or C”
Text(stringArray, format: .list(type: .or))

<aside> 💡

用 + 号实现文本连接的 API 已经过时,不要再用。最佳做法是始终优先使用字符串插值,以确保语法正确且翻译自然。具体查看:

https://nilcoalescing.com/blog/TextConcatenationVsTextInterpolationInSwiftUI/?utm_source=substack&utm_medium=email

</aside>


使用第三方字体

<aside> 💡 关于 iOS 设备默认的可用字体可以看这: http://iosfonts.com/ 。如果没有合适的字体,你还可以使用自己下载的合法的第三方字体。

</aside>

  1. 首先下载字体文件,并将字体文件拖到 Xcode 项目文件夹里。这里请采用 iOS 原生支持的 .ttf.otf 字体,不要用其他格式的。在弹窗中接受默认设置,将文件复制到项目。选择每一个字体文件,在项目右侧的文件检查器窗格中检查:字体文件是否已正确关联到项目的 Target。

  2. 修改 Info.plist 属性列表,新增 【Fonts provided by application】 字段

    点击最左边导航中的项目根图标,找到相应 Targets,点击项目名,在右边找到 info 子菜单。在 Custom iOS Target Properties 中,右键新增一行 Fonts provided by application 。然后在里面新增 item,有几个字体就新增几项

  3. 然后在 item 后面的值里填上字体的名称即可,注意这里不写 PostScript 名称,只写字体文件名(如 Oswald-Bold.ttf),大小写敏感

  4. 在视图中这样使用 .font(.custom("DINNext-Medium", size: 24)) ,注意这里写的是字体 PostScript 名称(在字体册中可以 ⌘ + I 看到)

  5. 可以制作自定义修饰符 ViewModifier 方便使用(其中字体名不需要加后缀)

    struct CustomFontModifier : ViewModifier {
        var size : CGFloat = 28
        func body(content: Content) -> some View {
            content
                .font(.custom("Oswald-Regular", size: size))
        }
    }
    

使用 redacted 显示占位符

// 1. 将文本显示为占位符
// SwiftUI 允许将文本标记为视图中的占位符,这意味着它会被渲染,但会被灰色遮盖以表明它不是最终内容。
// 这是通过 redacted(reason:) 修饰符以及可用于根据需要覆盖密文的 unredacted() 修饰符提供的。
Text("This is placeholder text").redacted(reason: .placeholder)

// 还可以加到父元素上
VStack(alignment: .leading) {
    Text("This is placeholder text This is placeholder telder tex")
    Text("And so is this")
}.redacted(reason: .placeholder)

// 2. 还可以查询从环境传入的任何编辑原因
struct ContentView: View {
    @Environment(\.redactionReasons) var redactionReasons
    let bio = "The rain in Spain falls mainly on the Spaniards"
    var body: some View {
        if redactionReasons == .placeholder {
            Text("Loading…")
        } else {
            Text(bio)
                .redacted(reason: redactionReasons)
        }
    }
}

// 3. 还允许将视图的某些部分标记为包含敏感信息,这使我们可以使用密文更轻松地隐藏或显示它
// 要使用此功能请将 privacySensitive() 修饰符添加到应隐藏的任何视图
// 然后在更高的视图层次结构中应用 .redacted(reason: .privacy) 修饰符
VStack {
    Text("Card number")
        .font(.headline)

    if redactionReasons.contains(.privacy) {
        Text("[HIDDEN]").privacySensitive()
    } else {
        Text("1234 5678 9012 3456").privacySensitive()
    }
}
.redacted(reason: .privacy)

// 4.默认情况下,隐私敏感上下文被灰色框屏蔽,但您也可以通过从环境中读取密文原因来提供自定义布局:
struct ContentView: View {
    @Environment(\.redactionReasons) var redactionReasons

    var body: some View {
        VStack {
            Text("Card number")
                .font(.headline)

            if redactionReasons.contains(.privacy) {
                Text("[HIDDEN]")
            } else {
                Text("1234 5678 9012 3456")
            }
        }
    }
}

呈现 Markdown 内容

Text 视图提供了两种使用 Markdown 设置文本样式的方法:直接在 Text 视图中和使用 AttributedString 。

直接在 Text 视图中使用 Markdown 对于静态文本来说很方便,但对于动态字符串或当您想要将样式应用于字符串的不同部分时,您需要使用 AttributedString 以编程方式。

// SwiftUI 内置了对 Markdown 渲染的支持,包括粗体、斜体、链接等
// 它实际上内置于 SwiftUI 的 Text 视图中,因此您可以编写如下代码:
VStack {
    Text("This is regular text.")
    Text("* This is **bold** text, this is *italic* text, and this is ***bold, italic*** text.")
    Text("~~A strikethrough example~~")
    Text("`Monospaced works too`")
    Text("Visit Apple: [click here](<https://apple.com>)")

    // 该链接是可自动点击的。默认情况下,Markdown 链接将使用应用程序的强调色,但可以使用 tint() 修饰符更改它:
    Text("Visit Apple: [click here](<https://apple.com>)")
        .tint(.indigo)

    // 注意:不支持图像

}

// 支持自动 Markdown 转换是因为 SwiftUI 将这些字符串解释为 LocalizedStringKey 实例(即可以由应用程序本地化的字符串)
// 这意味着如果您想从属性或变量创建 Markdown 文本,您应该将其显式标记为 LocalizedStringKey 以获得 Markdown 渲染:

struct ContentView: View {
    let markdownText: LocalizedStringKey = "* This is **bold** text, this is *italic* text, and this is ***bold, italic*** text."
    var body: some View {
        Text(markdownText)
    }
}

// 如果希望原始文本保持不变(即将原始的、未格式化的 Markdown 符号保留在原处),只需删除 LocalizedStringKey 注释即可
// 或者,您可以使用 Text(verbatim:) 初始值设定项完全禁用 Markdown 和本地化。

嵌入 SF 符号

当需要将文本与 SF 图标结合用作按钮标签或列表行标题时,使用 Label 视图是标准做法。但在某些情况下,可能需要更精确地控制图标的位置,例如,当我们想将 SF 图标插入字符串中时,实现此目的的最直接方法是在传递给 Text 字符串字面量中插入 Image 视图。

Text("Watch your gear! The kea \(Image(systemName: "bird")) are known to steal anything shiny.")

但是这种方法仅适用于字符串字面量,不适用于字符串变量。因为这种情况下,字符串字面量会自动转换为 LocalizedStringKey ,而 LocalizedStringKey 支持 Image 视图插值。在底层,它会调用 [init(_:tableName:bundle:comment:)](<https://developer.apple.com/documentation/swiftui/text/init%28_%3Atablename%3Abundle%3Acomment%3A%29>)  方法来初始化 Text 视图,而 LocalizedStringKey 又是通过 init(stringInterpolation:) 方法创建的。

如果我们需要在 Text 视图之外定义带有图像的字符串,我们可以先将其初始化为 LocalizedStringKey ,然后将其传递给 Text 。例如参考下面这样的代码:

struct ContentView: View {

    private let key: LocalizedStringKey = """
    Watch your gear! The kea \(
        Image(systemName: "bird")
    ) are known to steal anything shiny.
    """
    
    var body: some View {
        Text(key)
    }
    
}

如果要为插值图像添加额外的样式,我们必须先将它们包装在另一个 Text 视图中。类似下面这样,虽然有点啰嗦,但有效。

private let key: LocalizedStringKey = """
		Watch your gear! The kea \(
		    Text(Image(systemName: "bird")).foregroundStyle(.green)
		) are known to steal anything shiny.
"""

遗憾的是,SwiftUI 不支持通过 AttributedString API 将符号图像添加到文本中,否则可以实现更动态的插入方法。


处理英文复数

https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflection/

https://samwize.com/2025/04/11/plurals-with-swiftui/?utm_source=substack&utm_medium=email

解决英文复数的展示问题,一般有以下做法:

1. 最懒的办法

最偷懒的做法是写成固定文本的 cup(s)

2. 使用三元运算符

第二种做法是使用三元运算符来纠正这个问题,如下所示:

Text(
    question.questionNotes.count == 1 ?
    NSLocalizedString("one_note", comment: "") :
    String(format: NSLocalizedString("multiple_notes", comment: ""), question.questionNotes.count)
)

// 这种做法要注意本地化语言时,要添加两种状态的文案
// 英文 Localizable.strings (English)
one_note = "1 note";
multiple_notes = "%d notes";

3. 使用特殊 Markdown 写法

Foundation 框架有一个称为自动语法协议的功能,它可以确保文本遵循复数和性别等语法规则。它与 SwiftUI 无缝协作,允许我们直接在文本视图中处理复数,而无需任何额外的手动逻辑。要使文本自动调整为复数值,我们可以指定它使用 inflection rule,并定义其范围:

Text("You read ^[\(bookCount) book](inflect: true) this year!")

Stepper("^[\(coffeeAmount) cup](inflect: true)", value: $coffeeAmount, in: 1...20)

GI_OGwIb0AAGFnP.jpg

SwiftUI 将此语法识别为自定义 Markdown 属性,当使用字符串文本创建文本视图时,SwiftUI 会将字符串视为 LocalizedStringKey 并解析它包含的 Markdown。它识别字符串中的变形属性,并使用 Foundation 的自动语法一致性功能在渲染过程中应用必要的调整。

这种集成使 SwiftUI 中的复数处理变得简单而高效。自动语法一致性功能和 inflection 属性是在 iOS 15 中引入的,最初支持英语和西班牙语。多年来,语法引擎已扩展到包含其他语言,从 iOS 18 开始,它还支持德语、法语、意大利语、葡萄牙语、印地语和韩语,使其在多语言应用程序中更加通用。

<aside> 💡

当使用方案 3 时有一个问题,例如: .accessibilityHint("^[\(filter.activeIssuesCount) issue](inflect: true)")

这时如果启用 VoiceOver 将看到错误:ERROR: ^[%lld issue] (inflect: true) not found in table Localizable of bundle

因为 [\(filter.activeIssuesCount) issue] 的其中一部分是特殊 Markdown 语法,另一部分 %lld 是整数的 Localizable.strings 格式,这是我们的字符串插值中要替换的格式。虽然可以将此文本添加到 Localizable.strings 文件中,但这不是一个长久的解决方案,因为自动语法协议不支持其他语言,例如匈牙利语!

英语的复数形式不规则,即使涉及0个项目也使用复数形式。但也有更复杂的语言,例如,阿拉伯语有一种形式表示一个对象的零,另一种形式表示一个,另一个形式表示两个,另一个形式表示几个对象,另一种形式表示许多对象,还有一个形式表示所有其他计数。我们可以尝试用 Swift 对这些规则进行编码,但这比你想象的要困难得多。例如,俄语也有特殊的复数规则,当存在许多对象时,但俄语中“许多”的定义与阿拉伯语中的“许多”不同!

</aside>

4. 最好的办法

基于以上方案的语言本地化问题,因此需要一个更好的解决方案。终极保险的方法还是用 Localizable.stringsdict

具体参照这里:4. 解决英语复数的问题 (这样不管是展示复数,还是本地化,都没有问题)


Label 视图

// 方式:使用 SF 符号
Label("Welcome to the app", systemImage: "folder.circle")

// 方式:使用自己的图片资源
Label("Welcome to the app", image: "star")

// 方式:Label 包含 title 和 icon 两个属性,可以快速放置 图片 和 文字,而不必使用布局视图
Label(
	title:{ 
		Text("Delete").fontWeight(.semibold)
	}
	icon:{
		Image(systemName:"trash").font(.title)
	}
)

// 方式:还可以为文本和图像提供完全自定义的视图
Label {
    Text("Paul Hudson")
        .foregroundStyle(.primary)
        .font(.largeTitle)
        .padding()
        .background(.gray.opacity(0.2))
        .clipShape(Capsule())
} icon: {
    RoundedRectangle(cornerRadius: 10)
        .fill(.blue)
        .frame(width: 64, height: 64)
}

通过 .labelStyle() 修饰符,可以控制标签的显示方式,其支持参数值有automatic.titleOnly 、 .iconOnly  和  .titleAndIcon

VStack {
    Label("Text Only", systemImage: "heart")
        .font(.title)
        .labelStyle(.titleOnly)

    Label("Icon Only", systemImage: "star")
        .font(.title)
        .labelStyle(.iconOnly)

    Label("Both", systemImage: "paperplane")
        .font(.title)
        .labelStyle(.titleAndIcon)
}

format 参数格式化

很多时候,在向用户显示或将值插入应用程序之前,必须对其进行格式化。例如,温度、百分比、格式正确的日期、姓名等等,都需要格式化。

.number 显示为数字

// 以下两种写法都会展示 12,450,但第二种是真正将字符串转成数字,第一种是不会校验的
let count = 12450
Text("\(count)“)
Text(count, format: .number)

// 如果是对字符串其中的部分插值,转化为数字,是这样写
Text("转换结果是: \(finalValue.formatted(.number))")

Text(count, format: .number.notation(.compactName))
// Output: 12K
 
Text(count, format: .number.notation(.scientific))
// Output: 1.245E4
 
Text(count, format: .number.grouping(.never))
// Output: 12450 (显性要求不展示分组符号)
 
Text(count, format: .number.sign(strategy: .always()))
// Output: +12,450

Text(count, format: .number.notation(.scientific).sign(strategy: .always()))
// 还可以结合多个条件来使用
// 以小数精度显示数字,.number 参数后的 precision 方法会接收一个分数长度作为单独参数
let rating = 4.6789
Text(rating, format: .number.precision(.fractionLength(2)))

// 还可以指定小数长度范围,例如这里接受 1 - 2 位小数,都是可以的
TextField("Line Height", value: $lineHeight, format: .number.precision(.fractionLength(1…2)))

// 保留小数点后两位,另外一种写法,用 String 构造函数
Text("转换结果是: \(String(format: "%.2f", finalValue))")

.percent 显示为百分比

无需任何额外操作即可将任何 Double 类型值表示为百分比。请注意,显示的百分比始终是原始值乘以 100。

let progress = 0.72
Text(progress, format: .percent)
// Output: 72%

在 TextField 输入框控件中我们也可以读取百分比值,输入框会自动在数字后添加“%”符号,并丢弃任何无效字符。在这种情况下,即使存储的值是百分比除以 100,我们也会写入实际的百分比。