我要定位几十个文本,以使其领先的基线(lastTextBaseline
)处于特定坐标。 position
只能设置中心。例如:
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 {
var body: some View {
ZStack {
ForEach(locations) { run in
Text(verbatim: run.string)
.font(.system(size: 48))
.border(Color.green)
.rotationEffect(.radians(run.angle))
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
这将字符串定位为使其中心位于所需的点(标记为红色圆圈):
我想对此进行调整,以使领先基准位于该红点处。在此示例中,正确的布局将使字形在右上方移动。
我尝试将.topLeading
对齐方式添加到ZStack,然后使用offset
而不是position
。这将使我根据最领先的拐角对齐,但这不是我要布局的拐角。例如:
ZStack(alignment: .topLeading) { // add alignment
Rectangle().foregroundColor(.clear) // to force ZStack to full size
ForEach(locations) { run in
Text(verbatim: run.string)
.font(.system(size: 48))
.border(Color.green)
.rotationEffect(.radians(run.angle), anchor: .topLeading) // rotate on top-leading
.offset(x: run.point.x, y: run.point.y)
}
}
我还尝试过更改文本的“顶部”对齐指南:
.alignmentGuide(.top) { d in d[.lastTextBaseline]}
这会移动红点而不是文本,所以我不认为这是正确的方法。
我正在考虑尝试自行调整位置,以考虑文本的大小(我可以使用Core Text进行预测),但我希望避免计算很多额外的边界框。
答案 0 :(得分:4)
据我所知,对齐向导还不能以这种方式使用-到目前为止。希望这会很快到来,但是与此同时,我们可以做一些填充和叠加技巧以获得预期的效果。
CTFont
初始化我的Font
实例并以此方式获取指标。displayScale
环境值(以及派生的pixelLength
值)在操场甚至预览中默认情况下均未正确设置。因此,如果要使用代表性布局(FB7280058),则必须在这些环境中手动设置此设置。我们将结合许多SwiftUI功能来获得我们想要的结果。具体来说,是变换,叠加和GeometryReader
视图。
首先,我们将字形的基线与视图的基线对齐。如果我们具有字体的度量标准,则可以使用字体的“下降”来将字形向下移一点,以使其与基线齐平–我们可以使用padding
视图修饰符来帮助我们解决这个问题。
接下来,我们将在字形视图上覆盖一个重复视图。为什么?因为在叠加层中,我们能够获取下面视图的确切度量。实际上,我们的叠加层将是用户看到的唯一视图,原始视图将仅用于其指标。
几个简单的变换会将我们的叠加层放置在我们想要的位置,然后我们将隐藏其下方的视图以完成效果。
首先,我们将需要一些其他属性来帮助我们进行计算。在适当的项目中,您可以将其组织到视图修饰符或类似的视图中,但为简洁起见,我们会将其添加到现有视图中。
@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)
}()
然后进行一些计算。这将为文本视图计算一些EdgeInset,从而将文本视图的基线移至封闭的填充视图的底部边缘:
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
的Text视图:
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
}
glyphView(_:)
通话中采用我们的body
这一步很简单,让我们采用上面定义的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)
}
}
}
这使我们在这里:
接下来,我们移动文本视图的基线,以使其与封闭的填充视图的底部齐平。这只是在我们的新glyphView(_:)
函数中添加填充修饰符的一种情况,该函数利用了我们上面定义的填充计算。
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding) // Added padding modifier
}
请注意,字形现在如何与其封闭视图的底部齐平。
我们需要获取字形的指标,以便我们能够准确地放置它。但是,在列出视图之前,我们无法获得这些指标。解决此问题的一种方法是复制我们的视图,并使用一个视图作为原本可以隐藏的度量标准的来源,然后显示一个重复的视图,供我们使用收集的度量标准进行定位。
我们可以使用overlay修饰符和一个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)
利用我们现在可以使用的度量,我们可以将覆盖层向上和向右移动,以使字形视图的左下角位于我们的红色位置。
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)
现在我们的视线已经确定,我们终于可以旋转了。
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)
最后一步是隐藏源视图,并将覆盖字形设置为适当的颜色:
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,该API允许我们使用对齐锚来锚定我们的转换,否则我们可能会成功!
答案 1 :(得分:0)
已更新:您可以尝试以下变体
let font = UIFont.systemFont(ofSize: 48)
var body: some View {
ZStack {
ForEach(locations) { run in
Text(verbatim: run.string)
.font(Font(self.font))
.border(Color.green)
.offset(x: 0, y: -self.font.lineHeight / 2.0)
.rotationEffect(.radians(run.angle))
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
还有另一个有趣的变体,请使用ascender
而不是lineHeight
.offset(x: 0, y: -self.font.ascender / 2.0)
答案 2 :(得分:0)
此代码负责字体指标,并根据您的要求放置文本 (如果我正确理解了您的要求:-))
import SwiftUI
import PlaygroundSupport
struct BaseLine: ViewModifier {
let alignment: HorizontalAlignment
@State private var ref = CGSize.zero
private var align: CGFloat {
switch alignment {
case .leading:
return 1
case .center:
return 0
case .trailing:
return -1
default:
return 0
}
}
func body(content: Content) -> some View {
ZStack {
Circle().frame(width: 0, height: 0, alignment: .center)
content.alignmentGuide(VerticalAlignment.center) { (d) -> CGFloat in
DispatchQueue.main.async {
self.ref.height = d[VerticalAlignment.center] - d[.lastTextBaseline]
self.ref.width = d.width / 2
}
return d[VerticalAlignment.center]
}
.offset(x: align * ref.width, y: ref.height)
}
}
}
struct ContentView: View {
var body: some View {
ZStack {
Cross(size: 20, color: Color.red).position(x: 200, y: 200)
Cross(size: 20, color: Color.red).position(x: 200, y: 250)
Cross(size: 20, color: Color.red).position(x: 200, y: 300)
Cross(size: 20, color: Color.red).position(x: 200, y: 350)
Text("WORLD").font(.title).border(Color.gray).modifier(BaseLine(alignment: .trailing))
.rotationEffect(.degrees(45))
.position(x: 200, y: 200)
Text("Y").font(.system(size: 150)).border(Color.gray).modifier(BaseLine(alignment: .center))
.rotationEffect(.degrees(45))
.position(x: 200, y: 250)
Text("Y").font(.system(size: 150)).border(Color.gray).modifier(BaseLine(alignment: .leading))
.rotationEffect(.degrees(45))
.position(x: 200, y: 350)
Text("WORLD").font(.title).border(Color.gray).modifier(BaseLine(alignment: .leading))
.rotationEffect(.degrees(225))
.position(x: 200, y: 300)
}
}
}
struct Cross: View {
let size: CGFloat
var color = Color.clear
var body: some View {
Path { p in
p.move(to: CGPoint(x: size / 2, y: 0))
p.addLine(to: CGPoint(x: size / 2, y: size))
p.move(to: CGPoint(x: 0, y: size / 2))
p.addLine(to: CGPoint(x: size, y: size / 2))
}
.stroke().foregroundColor(color)
.frame(width: size, height: size, alignment: .center)
}
}
PlaygroundPage.current.setLiveView(ContentView())