SwiftUI 供给了强壮的布局才干,不过这些布局操作都是在视图之间进行的。当咱们想在 Text 中进行图文混排时,需求选用与视图布局不同的思路与操作办法。本文将首要介绍一些与 Text 有关的知识,并经过一个实践事例,为咱们整理出在 SwiftUI 中用 Text 完结图文混排的思路。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

一个和一组

在 SwiftUI 中,Text 是运用频率最高的几个组件之一,几乎所有的文字显现操作均由其完结。随着 SwiftUI 版本的不断提升,Text 的功用也得到继续地增强。除了基本的文本内容外,还供给了对 AttributedString、Image( 有限度 )、Fomatter 等类型的支撑。

假如 Text 视图无法在给定的主张宽度内显现悉数的内容,在主张高度允许的状况下( 没有限制高度或显现行数 ),Text 会对内容进行换行处理,经过多行显现的办法确保内容的完整性。上述特性有一个基本要求 —— 换行操作是在单一 Text 视图中进行的。鄙人面的代码中,虽然咱们经过布局容器视图将 Text 横向排列到一起,但 SwiftUI 仍会将它们视作多个 Text 视图( 一组 ),对每个 Text 分别进行换行操作:

struct TempView:View{
    let str = "道可道,十分道;名可名,十分名。"
    var body: some View{
        HStack{
            Text(str)
        }
        .padding()
    }
}

在 SwiftUI 中用 Text 实现图文混排

SwiftUI 供给了两种办法用以将多个 Text 转化成一个 Text:

  • 经过 LocalizedStringKey 插值的办法
HStack{
    let a = Text(str)
    let b = Text(str)
    let c = Text(str)
    Text("\(a) \(b) \(c)") 
}

在 SwiftUI 中用 Text 实现图文混排

咱们不仅能够经过插值的办法增加 Text ,还能够增加 Image、Date 等很多类型。王巍在 SwiftUI 中的 Text 插值和本地化 一文中对此做了翔实的介绍。

请注意:从第二个 Text 插值元素开端,有必要在插值符号 \( 前增加一个空格,不然会呈现显现反常( 这是一个继续了多个版本的 Bug )。尝试将上面的代码 Text("\(a) \(b) \(c)") 改成 Text("\(a)\(b)\(c)") 即可复现该错误。

  • 运用加法运算符
HStack{
    let a = Text(str)
    let b = Text(str)
    let c = Text(str)
    a + b + c
}

加法运算仅能够在 Text 类型之间进行。这意味着,当咱们对部分 Text 进行装备时,只能运用不改动 Text 类型的润饰器( 该原则同样适用于经过插值办法进行的合并 ),例如:

HStack{
    let a = Text(str)
        .foregroundColor(.red) // Text 专用版本,不改动 Text 类型
        .underline() // 不改动 Text 类型
//      .background(Color.yellow) // background 是针对 View 协议的润饰器,会改动 Text 的类型,无法运用
    let b = Text(str)
        .foregroundColor(.blue)
        .font(.title)
    let c = Text(str)
        .foregroundColor(.green)
        .bold()
    a + b + c
}

在 SwiftUI 中用 Text 实现图文混排

假如你常常有组成复杂文本的需求,能够创立一个成果结构器来简化该进程:

@resultBuilder
enum TextBuilder {
    static func buildBlock(_ components: Text...) -> Text {
        components.reduce(Text(""),+)
    }
}

运用该结构器,咱们能够愈加清晰、方便地组成复杂文本:

@TextBuilder
func textBuilder() -> Text {
    Text(str)
        .foregroundColor(.red)
        .underline()
    Text(str)
        .foregroundColor(.blue)
        .font(.title)
    Text(str)
        .foregroundColor(.green)
        .bold()
}

能够阅览 掌握 Result builders 一文,了解更多有关结构结构器方面的内容

在 Text 中运用 SF Symbols

SF Symbols 是苹果为开发者带来的一份厚礼,让开发者能够在苹果生态中近乎免费地运用由专业设计师创立的海量图标。到 2022 年,SF Symbols 现已具有了超越 4000 个符号,每个符号均具有九种重量和三种份额,并可主动与文本标签对齐。

在 SwiftUI 中,咱们需求经过 Image 来显现 SF Symbols,并可运用一些润饰器来对其进行设置:

Image(systemName: "ladybug")
    .symbolRenderingMode(.multicolor) // 指定烘托形式,Image 专用润饰器 ,Image 类型不产生改动
    .symbolVariant(.fill) // 设置变体 ,该润饰器适用于 View 协议,Image 类型产生了改动
    .font(.largeTitle) // 适用于 View 的润饰器,非 Text 专用版本

在 SwiftUI 中用 Text 实现图文混排

SF Symbols 供给了与苹果渠道的系统字体 San Francisco 无缝集成的才干,Text 会在排版进程中将其视为普通文本而统一处理。上文中介绍的两种办法均适用于将 SF Symbols 增加到 Text 中:

let bug = Image(systemName: "ladybug.fill") // 因为 symbolVariant 会改动 Image 的类型,因而咱们选用直接在称号中增加变体的办法来保持类型的稳定
    .symbolRenderingMode(.multicolor) // 指定烘托形式,Image 专用润饰器 ,Image 类型不产生改动
let bugText = Text(bug)
    .font(.largeTitle) // Text 专用版本,Text 类型不产生改动
// 经过插值的办法
Text("Hello \(bug)") // 在插值中运用 Image 类型,因为 font 会改动 Image 的类型,因而无法独自修正 bug 的巨细
Text("Hello \(bugText)") // 在插值中运用 Text,font( Text 专用润饰器 )不会改动 Text 类型,因而能够独自调整 bug 的巨细
// 运用加法运算符
Text("Hello ") + bugText 

在 SwiftUI 中用 Text 实现图文混排

能够说,在 Text 中,能够直接运用 Image 类型这个功用首要就是为 SF Symbols 而供给的。在或许的状况下,经过 Text + SF Symbols 的组合来完结图文混排是最佳的处理计划。

struct SymbolInTextView: View {
    @State private var value: Double = 0
    private let message = Image(systemName: "message.badge.filled.fill") // 
        .renderingMode(.original)
    private let wifi = Image(systemName: "wifi") // 
    private var animatableWifi: Image {
        Image(systemName: "wifi", variableValue: value)
    }
    var body: some View {
        VStack(spacing:50) {
            VStack {
                Text(message).font(.title) + Text("文字与 SF Symbols 混排。\(wifi) Text 会将插值图片视作文字的一部分。") + Text(animatableWifi).foregroundColor(.blue)
            }
        }
        .task(changeVariableValue)
        .frame(width:300)
    }
    @Sendable
    func changeVariableValue() async {
        while !Task.isCancelled {
            if value >= 1 { value = 0 }
            try? await Task.sleep(nanoseconds: 1000000000)
            value += 0.25
        }
    }
}

在 SwiftUI 中用 Text 实现图文混排

虽然咱们能够运用 SF Symbols 应用程序来修正或创立自定义符号,但因为受颜色、份额等方面的限制,在相当多的场合中, SF Symbols 仍无法满意需求。此时,咱们需求运用真正的 Image 来进行图文混排工作。

VStack {
    let logo = Image("logo")  // logo 是一个 80 x 28 尺度的图片,默许状况下,title 的高度为 28
    Text("欢迎拜访 \(logo) !")
        .font(.title)
    Text("欢迎拜访 \(logo) !")
        .font(.body)
}

在 SwiftUI 中用 Text 实现图文混排

当在 Text 中运用真正的 Image ( 非 SF Symbols )时,Text 只能以图片的原始尺度进行烘托( SVG、PDF 以标示尺度为准 ),图片的尺度并不会随字体尺度巨细的改动而改动

另一方面,因为 Image( 非 SF Symbols )的 textBaseline 在默许状况下是与其 bottom 一致的,这导致在与 Text 中其他的文字进行混排时,图片与文字会因为基准线的不同而产生上下错位的状况。咱们能够经过运用 Text 专属版本的 baselineOffset 润饰器对其进行调整。

let logo = Text(Image("logo")).baselineOffset(-3) // Text 版本的润饰器,不会改动 Text 类型,运用 alignmentGuide 进行修正会更改类型
Text("欢迎拜访 \(logo) !")
    .font(.title)

在 SwiftUI 中用 Text 实现图文混排

有关 baseline 对齐线方面的内容,请阅览 SwiftUI 布局 —— 对齐 一文

再次着重,咱们只能运用不会改动 Text 或 Image 类型的润饰器。例如 frame、scaleEffect、scaleToFit、alignmentGuide 之类会改动类型状态的润饰器将导致无法进行 Text 插值以及加法运算操作!

如此一来,为了能让视图与文字完美地进行匹配,咱们需求为不同尺度的文字预备不同尺度的视图

动态类型( 主动缩放字体 )

苹果一直很努力地改进其生态的用户体会,考虑到用户与显现器的距离、视力、运动与否,以及环境照明条件等要素,苹果为用户供给了动态类型功用来提高内容的可读性。

动态类型( Dynamic Type )功用允许运用者在设备端设置屏幕上显现的文本内容的巨细。它能够帮助那些需求较大文本以提高可读性的用户,还能满意那些能够阅览较小文字的人,让更多信息呈现在屏幕上。支撑动态类型的应用程序也会为运用者供给一个更一致的阅览体会。

用户能够在操控中心或经过【设置】—【辅助功用】—【显现与文字巨细】—【更大字体】来更改单个或悉数应用程序的文字显现巨细。

在 SwiftUI 中用 Text 实现图文混排

从 Xcode 14 开端,开发者能够在预览中快速检查视图在不同动态类型下的表现。

Text("欢迎拜访 \(logo) !")
    .font(.title)  // title 在不同动态形式下,显现的尺度不同。

在 SwiftUI 中用 Text 实现图文混排

在 SwiftUI 中,除非进行了特别的设置,不然所有字体的尺度都会跟随动态类型的改动而改动。从上图中能够看出,动态类型仅对文本有用,Text 中的图片尺度并不会产生改动

在运用 Text 完结图文混排时,假如图片不能伴随文本的尺度改动而改动,就会呈现上图中的成果。因而,咱们有必要经过某种手段让图片的尺度也能主动适应动态类型的改动。

运用 SwiftUI 供给的 @ScaledMetric 属性包装器,能够创立能够跟随动态类型主动缩放的数值。relativeTo 参数能够让数值与特定的文本风格的尺度改动曲线相相关。

@ScaledMetric(relativeTo: .body) var imageSize = 17

不同的文本风格( Text Style )用以呼应动态类型改动的尺度数值曲线并不相同,详情请阅览苹果的 设计文档

struct TempView: View {
    @ScaledMetric(relativeTo:.body) var height = 17 // body 的默许高度
    var body: some View {
        VStack {
            Image("logo")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(height:height)
            Text("欢迎拜访!")
                .font(.body)
        }
        .padding()
    }
}

上面的代码,经过 ScaledMetric 将图片的高度与 .body 文本风格的尺度进行了相关,当动态类型产生改动时,图片的尺度也会随之做出调整。

在 SwiftUI 中用 Text 实现图文混排

惋惜的是,因为 frame 会更改 Image 的类型,因而咱们无法将经过 frame 动态更改尺度后的图片嵌入到 Text 中,以完结可动态调整尺度的图文混排。

运用 .dynamicTypeSize(DynamicTypeSize.xSmall...DynamicTypeSize.xxxLarge) 能够让视图只在指定的动态类型范围内产生改动。

运用 .font(custom(_ name: String, size: CGFloat)) 设置的自定义尺度的字体也会在动态类型改动时主动调整尺度。

运用 .font(custom(_ name: String, size: CGFloat, relativeTo textStyle: Font.TextStyle)) 能够让自定义尺度的字体与某个预设文本风格的动态类型尺度改动曲线相相关。

运用.font(custom(_ name: String, fixedSize: CGFloat)) 将让自定义尺度字体忽略动态类型的改动,尺度始终不产生改动。

一个有关图文混排的问题

前几天在 聊天室 中,一个朋友询问 SwiftUI 是否能完结下图中 tag( 超市标签 )+ 产品介绍的版式作用。我直接回复没有问题,但直到考虑具体完结时才发现,状况没有那么简略。

在 SwiftUI 中用 Text 实现图文混排

  • 标签选用了圆角背景,意味着依据 AttributedString 的处理计划被排除
  • 标签特定的尺度与内容,意味着依据自定义 SF Symbols 的处理计划被排除
  • 经过在 Text 中增加 Image 进行图文混排,需求考虑怎么处理动态类型改动的问题( 不或许预生成如此多尺度的图片 )
  • 是否能够不经过预制标签图片的办法( 用动态视图 )来处理当时问题

下文中,我将供给三种处理思路和对应代码,利用不同的办法来完结当时的需求。

限于篇幅,下文中将不会对典范代码做翔实的讲解,主张你结合本文附带的 典范代码 一并阅览接下来的内容。从 Xcode 运转典范代码,动态创立的图片或许并不会当即显现出来( 这是 Xcode 的问题 )。直接从模拟器或实机上再次运转将不会呈现上述推迟现象。

计划一:在 Text 中直接运用图片

计划一的处理思路

既然为不同的动态类型供给不同尺度的图片能够满意 Text 图文混排的需求,那么计划一就以此为基础,依据动态类型的改动主动对给定的预制图片进行等份额缩放即可。

  • 从应用程序或网络上获取标签图片
  • 当动态类型改动时,将图片缩放至与相关的文本风格尺度一致
VStack(alignment: .leading, spacing: 50) {
            TitleWithImage(title: "佳农 马来西亚冷冻 猫山王阅读果肉 D197", fontStyle: .body, tagName: "JD_Tag")
            TitleWithImage(title: "佳农 马来西亚冷冻 猫山王阅读果肉 D197", fontStyle: .body, tagName: "JD_Tag")
                .environment(\.sizeCategory, .extraExtraExtraLarge)
        }

在 SwiftUI 中用 Text 实现图文混排

计划一的注意事项

  • 为了确保图片缩放后的质量,典范中选用了 SVG 格局
  • 鉴于 SwiftUI 供给的图片缩放 modifier 均会改动类型,缩放操作将运用 UIGraphicsImageRenderer 针对 UIImage 进行
extension UIImage {
    func resized(to size: CGSize) -> UIImage {
        return UIGraphicsImageRenderer(size: size).image { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }
    }
}
  • 因为运用了 UIFont.preferredFont 获取 Text Style 的尺度,因而 Text Style 参数选用了 UIFont.TextStyle 类型。
  • 让 Image 的初始高度与给定的 Text Style 一致,并经过运用 @ScaledMetric 让两者的尺度改动保持同步
let uiFont = UIFont.preferredFont(forTextStyle: fontStyle)
pointSize = uiFont.pointSize
textStyle = Font.TextStyle.convert(from: fontStyle)
_fontSize = ScaledMetric(wrappedValue: pointSize, relativeTo: textStyle)
  • 运用 .font(.custom("", size: pointSize, relativeTo: textStyle)) 设置字体尺度,并与给定的 Text Style 进行相关
  • 正确运用 task 润饰器,以确保尺度缩放操作在后台线程进行,削减对主线程的影响
@Sendable
func resizeImage() async {
    if var image = UIImage(named: tagName) {
        let aspectRatio = image.size.width / image.size.height
        let newSize = CGSize(width: aspectRatio * fontSize, height: fontSize)
        image = image.resized(to: newSize)
        tagImage = Image(uiImage: image)
    }
}
.task(id: fontSize, resizeImage)
  • 经过 baselineOffset 修正图片的文本基线。偏移值应该依据不同的动态类型进行微调( 本人偷闲,典范代码中运用了固定值 )

计划一的优缺点

  • 计划简略,完结容易

  • 因为图片需求预制,因而不适合标签种类多,且常常变化的场景

  • 在无法运用矢量图片的状况下,为了确保缩放后的作用,需求供给分辨率较高的原始图片,这样会造成更多的系统负担

计划二:在 Text 上运用掩盖视图

计划二的处理思路

  • 不运用预制图片,经过 SwiftUI 视图创立标签
  • 依据标签视图的尺度创立空白占位图片
  • 在 Text 中增加占位图片,进行混排
  • 运用 overlay 将标签视图定位在 leadingTop 方位,掩盖于占位图片上
TitleWithOverlay(title: "佳农 马来西亚冷冻 猫山王阅读果肉 D197", tag: "京东超市", fontStyle: .body)
TitleWithOverlay(title: "佳农 马来西亚冷冻 猫山王阅读果肉 D197", tag: "京东超市", fontStyle: .body)
    .environment(\.sizeCategory, .extraExtraExtraLarge)

在 SwiftUI 中用 Text 实现图文混排

计划二的注意事项

  • 运用 fixedSize 制止标签视图自行呼应动态类型。标签视图 TagView 中的文字尺度完全由 TitleWithOverlay 操控
Text(tag)
    .font(.custom("", fixedSize: fontSize))
  • 运用 alignmentGuide 微调标签视图的方位,使其与 Text 的文字对齐。与计划一类似,offset、padding、fontSize 等最好依据动态类型进行微调( 作者偷闲,没有微调。不过最终作用还能够接受 )
TagView(tag: tag, textStyle: textStyle, fontSize: fontSize - 6, horizontalPadding: 5.5, verticalPadding: 2)
    .alignmentGuide(.top, computeValue: { $0[.top] - fontSize / 18 })
  • 当 fontSize ( 动态类型下当时的文本尺度 )产生改动时,更新标签视图尺度
Color.clear
    .task(id:fontSize) { // 运用 task(id:)
        tagSize = proxy.size
    }
  • 当标签视图尺度 tagSize 产生改动时,从头创立占位图片
.task(id: tagSize, createPlaceHolder)
  • 正确运用 task 润饰器,以确保创立占位图片的操作在后台线程进行,削减对主线程的影响
extension UIImage {
    @Sendable
    static func solidImageGenerator(_ color: UIColor, size: CGSize) async -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        let image = UIGraphicsImageRenderer(size: size, format: format).image { rendererContext in
            color.setFill()
            rendererContext.fill(CGRect(origin: .zero, size: size))
        }
        return image
    }
}
@Sendable
func createPlaceHolder() async {
    let size = CGSize(width: tagSize.width, height: 1) // 仅需横向占位,高度够用就行
    let uiImage = await UIImage.solidImageGenerator(.clear, size: size)
    let image = Image(uiImage: uiImage)
    placeHolder = Text(image)
}

计划二的优缺点

  • 无须预制图片
  • 标签的内容、复杂度等不再受限
  • 仅适用于当时的特别事例( 标签在左上角 ),一旦改动标签的方位,此计划将不再有用( 其他方位很难在 overlay 中对齐 )

计划三:将视图转化成图片,插入 Text 中

计划三的处理思路

  • 与计划二一样,不运用预制图片,运用 SwiftUI 视图创立标签
  • 将标签视图转化成图片增加到 Text 中进行混排
TitleWithDynamicImage(title: "佳农 马来西亚冷冻 猫山王阅读果肉 D197", tag: "京东超市", fontStyle: .body)
TitleWithDynamicImage(title: "佳农 马来西亚冷冻 猫山王阅读果肉 D197", tag: "京东超市", fontStyle: .body)
    .environment(\.sizeCategory, .extraExtraExtraLarge)

在 SwiftUI 中用 Text 实现图文混排

计划三的注意事项

  • 确保在后台进程中进行视图转化成图片的操作
@Sendable
func createImage() async {
    let tagView = TagView(tag: tag, textStyle: textStyle, fontSize: fontSize - 6, horizontalPadding: 5.5, verticalPadding: 2)
    tagView.generateSnapshot(snapshot: $tagImage)
}
  • 转化图片的进程中需设置正确的 scale 值,以确保图片的品质
func generateSnapshot(snapshot: Binding<Image>) {
    Task {
        let renderer = await ImageRenderer(content: self)
        await MainActor.run {
            renderer.scale = UIScreen.main.scale // 设置正确的 scale 值
        }
        if let image = await renderer.uiImage {
            snapshot.wrappedValue = Image(uiImage: image)
        }
    }
}

计划三的优缺点

  • 无须预制图片
  • 标签的内容、复杂度等不再受限
  • 无须限制标签的方位,能够将其放置在 Text 中的任意方位
  • 因为典范代码中选用了 SwiftUI 4 供给的 ImageRenderer 完结视图至图片的转化,因而仅支撑 iOS 16+

在低版本的 SwiftUI 中,能够经过用 UIHostingController 包裹视图的办法,在 UIKit 下完结图片的转化操作。但因为 UIHostingController 仅能运转于主线程,因而这种转化操作对主线程的影响较大,请自行取舍

总结

在读完本文后,或许你的第一感受是 SwiftUI 好笨呀,竟然需求如此多的操作才干完结这种简略的需求。但能用现有的办法来处理这类实践问题,何尝又不是一种应战和乐趣?至少对我如此。

希望本文能够对你有所帮助。同时也欢迎你经过 Twitter、 Discord 频道与我进行交流。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】