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> 💡 关于 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))
        }
    }
    

使用 + 号连接文本

// 可以用 + 号连接多个文本(默认基线会对齐)
Text("Here is another ")
+ Text("example").foregroundColor(.red).underline()
+ Text("Notice").foregroundColor(.purple).bold()
+ Text("as a whole.").bold().italic()

// 可以用 + 号连接多个文本(设置基线偏移,解决基线不对齐问题,负数代表往下)
Text("100").bold()
	+ Text(" SWIFTUI ")
		.baselineOffset(-12)
	+ Text ("VIEWS").bold()
// 使用 + 号连接的带修饰符的 Text,修饰符只影响其所属 Text
Text("Hello Developer!") + Text("How are you?")
	.font(.title)
	.italic()

// 如果希望影响全部 Text,可以用括号把所有 Text 包括起来
(Text("Hello Developer!") + Text(" How are you?"))
	.font(.title)
	.italic()

<aside> 💡

【文本连接】和【字符串插值】两种方式都可以实现文本的连接,但它们之间对本地化翻译有一些不同的影响。总之,文本连接对于简单的样式场景非常有效,但最佳做法是始终优先使用文本插值,以确保语法正确且翻译自然。具体查看:

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

</aside>

使用 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 和本地化。

处理英文复数

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. 解决英语复数的问题 (这样不管是展示复数,还是本地化,都没有问题)


TextRender

TextRender 文本渲染器对象是 iOS 18 中的新功能。SwiftUI 的 TextRenderer 协议与 textRenderer() 修饰符相结合,能够完全控制文本的渲染方式,包括基于自定义逻辑的平滑动画渲染的能力。

https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-custom-text-effects-and-animations

https://www.youtube.com/watch?v=NkLdOwIoeaI

// 首先看一个简单的示例,该示例调整渲染文本中的每隔一行,使偶数行不透明,奇数行稍微半透明:
// - 几乎所有工作都在 ZebraStripeRenderer 结构中完成,符合 TextRenderer 协议
// - 该协议只有一个要求:处理文本渲染到图形上下文中的 draw(layout:in:) 方法。
// - Text.Layout 类型可以用作序列,因此在上面的代码中,我们循环遍历所有行,调整不透明度,然后一次渲染每一行
// - 每行本身就是一个包含零个或多个运行的序列,这些运行是具有相同样式的字母组,内部运行是单独的字形,它们是正在渲染的实际字母。
struct ZebraStripeRenderer: TextRenderer {
    func draw(layout: Text.Layout, in context: inout GraphicsContext) {
        for (index, line) in layout.enumerated() {
            if index.isMultiple(of: 2) {
                context.opacity = 1
            } else {
                context.opacity = 0.5
            }

            context.draw(line)
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("He thrusts his fists against the posts and still insists he sees the ghosts.")
            .font(.largeTitle)
            .textRenderer(ZebraStripeRenderer())
    }
}

// 为了帮助您直观地了解这一切是如何组合在一起的,我们创建一个简单的文本渲染器,在线条、线条和字形周围绘制方框,如下所示:
// 该代码运行时,您将看到各个组件周围绘制的红色、绿色和蓝色线条,因此您可以准确地看到它们的含义。
struct BoxedRenderer: TextRenderer {
    func draw(layout: Text.Layout, in context: inout GraphicsContext) {
        for line in layout {
            for run in line {
                for glyph in run {
                    context.stroke(Rectangle().path(in: glyph.typographicBounds.rect), with: .color(.blue), lineWidth: 2)
                }
                context.stroke(Rectangle().path(in: run.typographicBounds.rect), with: .color(.green), lineWidth: 2)
            }
            context.stroke(Rectangle().path(in: line.typographicBounds.rect), with: .color(.red), lineWidth: 2)
            context.draw(line)
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            (
                Text("This is a **very** important string") +
                    Text(" with lots of text inside.")
                    .foregroundStyle(.secondary)
            )
            .font(.largeTitle)
            .textRenderer(BoxedRenderer())
        }
    }
}
// 当符合 TextRenderer 时,就可以添加 animatableData 属性来控制值如何随时间变化,然后可以使用常规 SwiftUI 动画对其进行动画处理。
// 重要提示:渲染移动的文本时,最好使用.disablesSubpixelQuantization 选项,该选项允许在浮点位置渲染字母形状,而不是捕捉到最近的整数,从而实现更平滑的移动。

// 例如,我们创建一个简单的 WaveRenderer 结构,根据 strength 和 frequency 值上下弯曲字母:
struct WaveRenderer: TextRenderer {
    var strength: Double
    var frequency: Double

    var animatableData: Double {
        get { strength }
        set { strength = newValue }
    }

    func draw(layout: Text.Layout, in context: inout GraphicsContext) {
        for line in layout {
            for run in line {
                for (index, glyph) in run.enumerated() {
                    let yOffset = strength * sin(Double(index) * frequency)
                    var copy = context
                    copy.translateBy(x: 0, y: yOffset)
                    copy.draw(glyph, options: .disablesSubpixelQuantization)
                }
            }
        }
    }
}

// 提示:由于 GraphicsContext 使用值语义,因此通过复制上下文,您可以进行平移和缩放等更改,而不会影响其他绘图
// 在 SwiftUI 视图中使用它意味着传递一些随时间变化的属性,例如使用强度从 -10 移动到 +10 的动画:
struct ContentView: View {

    @State private var amount = -10.0

    var body: some View {
        Text("This is a very important string with lots of text inside. This is a very important string with lots of text inside.")
            .font(.largeTitle)
            .textRenderer(WaveRenderer(strength: amount, frequency: 0.5))
            .onAppear {
                withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
                    amount = 10
                }
            }
    }
}

// 或者我们可以通过对每个字母使用随机 Y 偏移来创建地震式效果:
struct QuakeRenderer: TextRenderer {
    var moveAmount: Double

    var animatableData: Double {
        get { moveAmount }
        set { moveAmount = newValue }
    }

    func draw(layout: Text.Layout, in context: inout GraphicsContext) {
        for line in layout {
            for run in line {
                for glyph in run {
                    var copy = context
                    let yOffset = Double.random(in: -moveAmount ... moveAmount)
                    copy.translateBy(x: 0, y: yOffset)
                    copy.draw(glyph, options: .disablesSubpixelQuantization)
                }
            }
        }
    }
}

struct ContentView: View {
    @State private var strength = 0.0
    var body: some View {
        Text("SHOCKWAVE")
            .font(.largeTitle.weight(.black).width(.compressed))
            .textRenderer(QuakeRenderer(moveAmount: strength))
            .onAppear {
                withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
                    strength = 10
                }
            }
    }
}

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)
}

设置 Label 样式

通过 .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 设置文本格式

SwiftUI 的文本视图能够通过设置 format 参数,从而显示日期、数组、测量值等。不过,此功能仅在 iOS 15 中可用,因此对于 iOS 14 和 13 支持,请参阅下面的 formatter 参数。