使用锚点定位视图

时间:2019-12-10 17:16:43

标签: swiftui

我要定位几十个文本,以使其领先的基线(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())

这将字符串定位为使其中心位于所需的点(标记为红色圆圈):

Image of Y, o, centered at their locations

我想对此进行调整,以使领先基准位于该红点处。在此示例中,正确的布局将使字形在右上方移动。

我尝试将.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)
     }
}

Image of Y, o, with top-leading at their locations

我还尝试过更改文本的“顶部”对齐指南:

.alignmentGuide(.top) { d in d[.lastTextBaseline]}

这会移动红点而不是文本,所以我不认为这是正确的方法。

我正在考虑尝试自行调整位置,以考虑文本的大小(我可以使用Core Text进行预测),但我希望避免计算很多额外的边界框。

3 个答案:

答案 0 :(得分:4)

据我所知,对齐向导还不能以这种方式使用-到目前为止。希望这会很快到来,但是与此同时,我们可以做一些填充和叠加技巧以获得预期的效果。

注意事项

  • 您将需要某种方式来检索字体指标–我正在使用CTFont初始化我的Font实例并以此方式获取指标。
  • 据我所知,游乐场并不总是代表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)
}()

然后进行一些计算。这将为文本视图计算一些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))
}

步骤2:在主要的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)
        }
    }
}

这使我们在这里:

Step 2

步骤3:基线偏移

接下来,我们移动文本视图的基线,以使其与封闭的填充视图的底部齐平。这只是在我们的新glyphView(_:)函数中添加填充修饰符的一种情况,该函数利用了我们上面定义的填充计算。

private func glyphView(for text: String) -> some View {
    Text(verbatim: text)
        .font(Font(baseFont))
        .padding(textPadding) // Added padding modifier
}

Step 3

请注意,字形现在如何与其封闭视图的底部齐平。

第4步:添加叠加层

我们需要获取字形的指标,以便我们能够准确地放置它。但是,在列出视图之前,我们无法获得这些指标。解决此问题的一种方法是复制我们的视图,并使用一个视图作为原本可以隐藏的度量标准的来源,然后显示一个重复的视图,供我们使用收集的度量标准进行定位。

我们可以使用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)

Step 4

步骤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)

Step 5

步骤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)

enter image description here

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

Step 7

最终代码

//: 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)

已更新:您可以尝试以下变体

Letter at point

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)

enter image description here

答案 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())

enter image description here