Text 文本

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

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

// Text也可以用于展示 Image:
Text(Image(systemName: "person"))
	.font(.system(.largeTitle, weight: .bold))

使用第三方字体

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

</aside>

  1. 首先下载字体文件,并拖入项目文件夹里。

    将字体文件拖到 Xcode 的项目导航器中。可以一次拖动包含 TrueType (.ttf) 或 OpenType (.otf) 文件的整个文件夹。接受默认设置;您确实想要复制项目、创建组并添加到当前目标。

    您可以通过在项目导航器中选择字体文件,然后使用 Command-Option-1 打开 Xcode 的文件检查器窗格来检查字体文件是否已正确添加到当前目标。在项目导航器中突出显示每个字体文件,并确保其目标成员资格显示您应用程序的目标。

  2. 制作修改器 ViewModifier (其中字体名不需要加后缀)

    struct CustomFontModifier : ViewModifier {
        var size : CGFloat = 28
        func body(content: Content) -> some View {
            content
                .font(.custom("Oswald-Regular", size: size))
        }
    }
    
  3. 修改 Info.plist 属性列表,新增 【Fonts provided by application】 字段

    点击最左边导航中的项目根图标,找到Targets,点击项目名,在右边找到 info 子菜单。在Custom iOS Target Properties中,新增 Fonts provided by application 。做完后,左侧的项目导航目录里会多一项 info 文件。在 Fonts provided by application 里新增 item,后面填上字体的名称即可,有几个字体就新增几项。

  4. 在字段里逐个填入 item,即字体文件名,如:Oswald-Bold.ttf

使用 + 号连接文本

// 可以用 + 号连接多个文本(默认基线会对齐)
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 参数。