因此,据我所知,目前还不能以这种方式使用对齐指南。希望这将很快到来,但与此同时,我们可以做一些填充和覆盖技巧来获得所需的效果。
注意事项
- 您需要有某种方法来检索字体指标 - 我正在使用
CTFont 来初始化我的 Font 实例并以这种方式检索指标。
- 据我所知,Playgrounds 并不总是代表 SwiftUI 布局在设备上的布局方式,并且会出现某些不一致的情况。我已经确定的一个是
displayScale 环境值(以及派生的 pixelLength 值)在游乐场甚至预览中默认设置不正确。因此,如果您想要有代表性的布局 (FB7280058),则必须在这些环境中手动设置。
概述
我们将结合一些 SwiftUI 功能来获得我们想要的结果。具体来说,转换、叠加和GeometryReader 视图。
首先,我们将字形的基线与视图的基线对齐。如果我们有字体的指标,我们可以使用字体的“下降”将字形向下移动一点,使其与基线齐平——我们可以使用padding 视图修饰符来帮助我们解决这个问题。
接下来,我们要用一个重复的视图覆盖我们的字形视图。为什么?因为在叠加层中,我们能够获取下方视图的确切指标。事实上,我们的叠加层将是用户看到的唯一视图,原始视图将仅用于其指标。
几个简单的变换将我们的叠加层定位在我们想要的位置,然后我们将隐藏位于下方的视图以完成效果。
第 1 步:设置
首先,我们需要一些额外的属性来帮助我们进行计算。在一个适当的项目中,您可以将其组织到视图修改器或类似内容中,但为了简洁起见,我们会将它们添加到我们现有的视图中。
@Environment(\.pixelLength) var pixelLength: CGFloat
@Environment(\.displayScale) var displayScale: CGFloat
我们还需要将字体初始化为CTFont,以便我们可以获取其指标:
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
然后进行一些计算。这会为文本视图计算一些 EdgeInsets,这些 EdgeInsets 将具有将文本视图的基线移动到封闭填充视图的底部边缘的效果:
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
我们还将向 CTFont 添加几个辅助属性:
extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
最后,我们创建了一个新的辅助函数来生成使用我们上面定义的CTFont 的文本视图:
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
}
第 2 步:在我们的主要 body 调用中采用我们的 glyphView(_:)
这一步很简单,让我们采用上面定义的glyphView(_:) 辅助函数:
var body: some View {
ZStack {
ForEach(locations) { run in
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
这让我们来到这里:
第 3 步:基线偏移
接下来我们移动文本视图的基线,使其与封闭填充视图的底部齐平。这只是向我们的新glyphView(_:)函数添加一个填充修饰符的例子,它利用了我们上面定义的填充计算。
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding) // Added padding modifier
}
注意字形现在如何与其封闭视图的底部齐平。
第 4 步:添加叠加层
我们需要获取字形的指标,以便我们能够准确地放置它。但是,在我们提出观点之前,我们无法获得这些指标。解决此问题的一种方法是复制我们的视图并将一个视图用作原本隐藏的指标来源,然后呈现我们使用收集的指标定位的重复视图。
我们可以使用覆盖修饰符和GeometryReader 视图来做到这一点。我们还将添加一个紫色边框,并将我们的叠加文本设为蓝色,以区别于上一步。
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
})
.position(run.point)
第 5 步:翻译
利用我们现在可以使用的指标,我们可以将叠加层向上和向右移动,以便字形视图的左下角位于我们的红色定位点。
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
})
.position(run.point)
第 6 步:旋转
现在我们有了最终可以旋转的位置。
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
第 7 步:隐藏我们的工作
最后一步是隐藏我们的源视图并将我们的叠加字形设置为正确的颜色:
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.hidden()
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.black)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
最终代码
//: A Cocoa based Playground to present user interface
import SwiftUI
import PlaygroundSupport
struct Location: Identifiable {
let id = UUID()
let point: CGPoint
let angle: Double
let string: String
}
let locations = [
Location(point: CGPoint(x: 54.48386479999999, y: 296.4645408), angle: -0.6605166885682314, string: "Y"),
Location(point: CGPoint(x: 74.99159120000002, y: 281.6336352), angle: -0.589411952788817, string: "o"),
]
struct ContentView: View {
@Environment(\.pixelLength) var pixelLength: CGFloat
@Environment(\.displayScale) var displayScale: CGFloat
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
var body: some View {
ZStack {
ForEach(locations) { run in
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.hidden()
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.black)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding)
}
}
private extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
PlaygroundPage.current.setLiveView(
ContentView()
.environment(\.displayScale, NSScreen.main?.backingScaleFactor ?? 1.0)
.frame(width: 640, height: 480)
.background(Color.white)
)
就是这样。它并不完美,但在 SwiftUI 为我们提供允许我们使用对齐锚来锚定我们的变换的 API 之前,它可能会让我们通过!