How to limit size of GeometryReader inside a VStack - swiftui

I would like to stop GeometryReader from interfering with my layout in a VStack but I'm not sure what the correct approach is.
Given the example of a simple graph with a title and caption:
struct Graph: View {
var body: some View {
HStack(spacing: 0) {
Color.red.frame(width: 100, height: 80)
Color.blue.frame(width: 100, height: 120)
Color.green.frame(width: 100, height: 180)
}
}
}
struct GraphExample: View {
var body: some View {
VStack {
Text("My graph")
// Graph represents a custom view with a natural and non-static height
Graph()
Text("My graph caption")
}
}
}
Produces this expected result:
But if we update the Graph view to use a GeometryReader to evenly split the graph across the width of the screen the layout changes.
struct Graph: View {
var body: some View {
GeometryReader { proxy in
HStack(spacing: 0) {
Color.red.frame(width: proxy.size.width / 3, height: 80)
Color.blue.frame(width: proxy.size.width / 3, height: 120)
Color.green.frame(width: proxy.size.width / 3, height: 180)
}
}
}
}
This makes the Graph view fill all available space in VStack
Is there a way to allow Graph view to just fill its natural height up, and not consume all available height while still using the GeometryReader.
Bear in mind that Graph could be any view here where the exact size is not known, so setting a static size is not possible. i.e. without GeometryReader, Graph can takes up only the needed amount of vertical space in the VStack.

Here is possible solution. Tested with Xcode 11.4 / iOS 13.4
struct Graph: View {
#State private var width = UIScreen.main.bounds.width // just initial constant
var body: some View {
HStack(spacing: 0) {
Color.red.frame(width: width / 3, height: 80)
Color.blue.frame(width: width / 3, height: 120)
Color.green.frame(width: width / 3, height: 180)
}.background(GeometryReader { gp -> Color in
let frame = gp.frame(in: .local)
DispatchQueue.main.async {
self.width = frame.size.width // << dynamic, on layout !!
}
return Color.clear
})
}
}

Related

Swift UI ScrollView layout behaviour

Does ScrollView in SwiftUI proposes no height for its children.
ScrollView {
Color.clear
.frame(idealHeight: 200)
.background(.blue)
}
will result in
As written in frame documentation
The size proposed to this view is the size proposed to the frame, limited by any constraints specified, and with any ideal dimensions specified replacing any corresponding unspecified dimensions in the proposal.
So does it mean scrollView doesn't propose any height?
A ScrollView with a vertical axis does not offer a height because its height is potentially infinite.
So the child views adopt their idealHeight. Which in the case of a view that normally uses all the height offered, like Color, or Rectangle, corresponds to the magic number 10.
struct SwiftUIView: View {
#State private var height: CGFloat = 0
var body: some View {
ScrollView {
Rectangle()
.background(
GeometryReader { geo in
Color.clear
.onAppear {
height = geo.size.height
}
}
)
}
.overlay(Text(height.description), alignment: .bottom)
}
}
Note that we also find this magic number 10 (putting aside the ScrollView) if we ask the same Rectangle to use its idealHeight, using the modifier .fixedSize
struct SwiftUIView101: View {
#State private var height: CGFloat = 0
var body: some View {
VStack {
Rectangle()
.fixedSize()
.background(
GeometryReader { geo in
Color.clear
.onAppear {
height = geo.size.height
}
}
)
Text(height.description)
}
}
}

Position an image randomly on a view in swiftui and pass the position of the image to another fullscreen transparent view

I have a view in SwiftUI. This view has some random images on it in various random positions. Check the code below.
struct ContentView: View {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { _ in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: CGFloat.random(in: 0..<screenWidth),
y: CGFloat.random(in: 0..<screenHeight)
)
}
}
.ignoreSafeArea()
}
}
I need to get the exact position of these random added images and pass the positions to another transparent view that shows up with a ZStack on top of the previous view. In the transparent popup fullscreen ZStack view i need to point to the position of the images i randomly put in the previous view using arrow images. Is this somehow possible in swiftui? I am new in swiftui so any help or suggestion appreciated.
Store the random offsets in a #State var and generate them in .onAppear { }. Then you can use them to position the random images and pass the offsets to the overlay view:
struct ContentView: View {
#State private var imageOffsets: [CGPoint] = Array(repeating: CGPoint.zero, count: 5)
#State private var showingOverlay = true
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
ForEach(0..<5) { index in
Image(systemName: "plus")
.frame(width: 30, height: 30)
.background(Color.green)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
.onAppear {
for index in 0..<5 {
imageOffsets[index] = CGPoint(x: .random(in: 0..<screenWidth), y: .random(in: 0..<screenHeight))
}
}
.overlay {
if showingOverlay {
OverlayView(imageOffsets: imageOffsets)
}
}
}
}
struct OverlayView: View {
let imageOffsets: [CGPoint]
var body: some View {
ZStack {
Color.clear
ForEach(0..<5) { index in
Circle()
.stroke(.blue)
.frame(width: 40, height: 40)
.position(
x: imageOffsets[index].x,
y: imageOffsets[index].y
)
}
}
.ignoresSafeArea()
}
}

SwiftUI Dynamic Image Sizing

Problem:I have a View that I needed to place multiple (2) views that contained: 1 Image + 1 Text. I decided to break that up into a ClickableImageAndText structure that I called on twice. This works perfectly if the image is a set size (64x64) but I would like this to work on all size classes. Now, I know that I can do the following:
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
but I am asking for both Different Size Classes and Same Size Classes such as the iPhone X and iPhone 13 which are the same.
Question:How do I alter the image for dynamic phone sizes (iPhone X, 13, 13 pro, etc) so it looks appropriate for all measurements?
Code:
import SwiftUI
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
struct InitialView: View {
var topView: some View {
Image("Empty_App_Icon")
.resizable()
.scaledToFit()
}
var bottomView: some View {
VStack {
ClickableImageAndText(
image: "Card_Icon",
text: "View Your Memories") {
print("Tapped on View Memories")
}
.padding(.bottom)
ClickableImageAndText(
image: "Camera",
text: "Add Memories") {
print("Tapped on Add Memories")
}
.padding(.top)
}
}
var body: some View {
ZStack {
GradientView()
VStack {
Spacer()
topView
Spacer()
bottomView
Spacer()
}
}
}
}
struct InitialView_Previews: PreviewProvider {
static var previews: some View {
InitialView()
}
}
Image Note:My background includes a GradientView that I have since removed (thanks #lorem ipsum). If you so desire, here is the GradientView code but it is unnecessary for the problem above.
GradientView.swift
import SwiftUI
struct GradientView: View {
let firstColor = Color(uiColor: UIColor(red: 127/255, green: 71/255, blue: 221/255, alpha: 1))
let secondColor = Color(uiColor: UIColor(red: 251/255, green: 174/255, blue: 23/255, alpha: 1))
let startPoint = UnitPoint(x: 0, y: 0)
let endPoint = UnitPoint(x: 0.5, y: 1)
var body: some View {
LinearGradient(gradient:
Gradient(
colors: [firstColor, secondColor]),
startPoint: startPoint,
endPoint: endPoint)
.ignoresSafeArea()
}
}
struct GradientView_Previews: PreviewProvider {
static var previews: some View {
GradientView()
}
}
Effort 1:Added a GeometryReader to my ClickableImageAndText structure and the view is automatically changed incorrectly.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader { reader in
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
}
Effort 2:Added a GeometryReader as directed by #loremipsum's [deleted] answer and the content is still being pushed; specifically, the topView is being push to the top and the bottomView is taking the entire space with the addition of the GeometryReader.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader{ geo in
VStack {
Image(image)
.resizable()
.scaledToFit()
//You can do this and set strict size constraints
//.frame(minWidth: 64, maxWidth: 128, minHeight: 64, maxHeight: 128, alignment: .center)
//Or this to set it to be percentage of the size of the screen
.frame(width: geo.size.width * 0.2, alignment: .center)
Text(text)
}.foregroundColor(.white)
//Everything moves to the left because the `View` expecting a size vs stretching.
//If yo want the entire width just set the View with on the outer most View
.frame(width: geo.size.width, alignment: .center)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
The possible solution is to use screen bounds (which will be different for different phones) as reference value to calculate per-cent-based dynamic size for image. And to track device orientation changes we wrap our calculations into GeometryReader.
Note: I don't have your images, so added white borders for demo purpose
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
#State private var size = CGFloat(32) // some minimal initial value (not 0)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
// .border(Color.white) // << for demo !!
.background(GeometryReader { _ in
// GeometryReader is needed to track orientation changes
let sizeX = UIScreen.main.bounds.width
let sizeY = UIScreen.main.bounds.height
// Screen bounds is needed for reference dimentions, and use
// it to calculate needed size as per-cent to be dynamic
let width = min(sizeX, sizeY)
Color.clear // % (whichever you want)
.preference(key: ViewWidthKey.self, value: width * 0.2)
})
.onPreferenceChange(ViewWidthKey.self) {
self.size = max($0, size)
}
.frame(width: size, height: size)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}

How to size a Image to 1/3 of the width of the row in SwiftUI?

Here's what i've got. I've tried putting the GeometryReader at different scopes but this is the best result so far.
You can the image is the correct width and height but the cell in the List is not tall enough.
struct AvtrCell: View
{
var avtr: Avatar
var body: some View {
GeometryReader { g in
NavigationLink(destination: AvtrForm(avtr: self.avtr)){
HStack() {
Image(self.avtr.img)
.resizable()
.aspectRatio(1, contentMode: .fill)
.cornerRadius(9)
.frame(width: g.size.width / 3)
VStack(alignment: .leading) {
Text("\(self.avtr.firstName()) \(self.avtr.lastName())").font(.headline)
Text("44hr Weekly Fasts")
Text("2000cal, prtn: 10g, fat: 10g, crb: 4g")
Text("Chest: 40, Waist: 32, Hips: 35")
Text("175ibs, 93% lean")
Text("pH: 9")
}
.font(.subheadline)
.lineLimit(1)
}
}
}
}
}
Please advise.
You can't avoid ambiguity working with GeometryReader being already inside row. So the solution is to pass needed parameter from outside.
Tested with replicated code on Xcode 11.4 / iOS 13.4
// Just tested replicated view
struct TestImageInRow: View {
var body: some View {
GeometryReader { g in
NavigationView {
List {
AvtrCell(imageWidth: g.size.width / 3)
AvtrCell(imageWidth: g.size.width / 3)
AvtrCell(imageWidth: g.size.width / 3)
AvtrCell(imageWidth: g.size.width / 3)
}
}
}
}
}
// Updated original cell
struct AvtrCell: View
{
var imageWidth: CGFloat // << here
var avtr: Avatar
var body: some View {
NavigationLink(destination: AvtrForm(avtr: self.avtr)){
HStack() {
Image(self.avtr.img)
.resizable()
.aspectRatio(1, contentMode: .fill)
.cornerRadius(9)
.frame(width: imageWidth) // << here
// ... other code

Positioning View using anchor point

I have several dozen Texts that I would like to position such that their leading baseline (lastTextBaseline) is at a specific coordinate. position can only set the center. For example:
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())
This locates the strings such that their center is at the desired point (marked as a red circle):
I would like to adjust this so that the leading baseline is at this red dot. In this example, a correct layout would move the glyphs up and to the right.
I have tried adding .topLeading alignment to the ZStack, and then using offset rather than position. This will let me align based on the top-leading corner, but that's not the corner I want to layout. For example:
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)
}
}
I've also tried changing the "top" alignment guide for the Texts:
.alignmentGuide(.top) { d in d[.lastTextBaseline]}
This moves the red dots rather than the text, so I don't believe this is on the right path.
I am considering trying to adjust the locations themselves to take into account the size of the Text (which I can predict using Core Text), but I am hoping to avoid calculating a lot of extra bounding boxes.
So, as far as I can tell, alignment guides can't be used in this way – yet. Hopefully this will be coming soon, but in the meantime we can do a little padding and overlay trickery to get the desired effect.
Caveats
You will need to have some way of retrieving the font metrics – I'm using CTFont to initialise my Font instances and retrieving metrics that way.
As far as I can tell, Playgrounds aren't always representative of how a SwiftUI layout will be laid out on the device, and certain inconsistencies arise. One that I've identified is that the displayScale environment value (and the derived pixelLength value) is not set correctly by default in playgrounds and even previews. Therefore, you have to set this manually in these environments if you want a representative layout (FB7280058).
Overview
We're going to combine a number of SwiftUI features to get the outcome we want here. Specifically, transforms, overlays and the GeometryReader view.
First, we'll align the baseline of our glyph to the baseline of our view. If we have the font's metrics we can use the font's 'descent' to shift our glyph down a little so it sits flush with the baseline – we can use the padding view modifier to help us with this.
Next, we're going to overlay our glyph view with a duplicate view. Why? Because within an overlay we're able to grab the exact metrics of the view underneath. In fact, our overlay will be the only view the user sees, the original view will only be utilised for its metrics.
A couple of simple transforms will position our overlay where we want it, and we'll then hide the view that sits underneath to complete the effect.
Step 1: Set up
First, we're going to need some additional properties to help with our calculations. In a proper project you could organise this into a view modifier or similar, but for conciseness we'll add them to our existing view.
#Environment(\.pixelLength) var pixelLength: CGFloat
#Environment(\.displayScale) var displayScale: CGFloat
We'll also need a our font initialised as a CTFont so we can grab its metrics:
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
Then some calculations. This calculates some EdgeInsets for a text view that will have the effect of moving the text view's baseline to the bottom edge of the enclosing padding view:
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
We'll also add a couple of helper properties to CTFont:
extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
And finally we create a new helper function to generate our Text views that uses the CTFont we defined above:
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
}
Step 2: Adopt our glyphView(_:) in our main body call
This step is simple and has us adopt the glyphView(_:) helper function we define above:
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)
}
}
}
This gets us here:
Step 3: Baseline shift
Next we shift the baseline of our text view so that it sits flush with the bottom of our enclosing padding view. This is just a case of adding a padding modifier to our new glyphView(_:)function that utilises the padding calculation we define above.
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding) // Added padding modifier
}
Notice how the glyphs are now sitting flush with the bottom of their enclosing views.
Step 4: Add an overlay
We need to get the metrics of our glyph so that we are able to accurately place it. However, we can't get those metrics until we've laid out our view. One way around this is to duplicate our view and use one view as a source of metrics that is otherwise hidden, and then present a duplicate view that we position using the metrics we've gathered.
We can do this with the overlay modifier together with a GeometryReader view. And we'll also add a purple border and make our overlay text blue to differentiate it from the previous step.
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 5: Translate
Making use of the metrics we now have available for us to use, we can shift our overlay up and to the right so that the bottom left corner of the glyph view sits on our red positioning spot.
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 6: Rotate
Now we have our view in position we can finally rotate.
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)
Step 7: Hide our workings out
Last step is to hide our source view and set our overlay glyph to its proper colour:
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)
The final code
//: 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)
)
And that's it. It's not perfect, but until SwiftUI gives us an API that allows us to use alignment anchors to anchor our transforms, it might get us by!
this code takes care of the font metrics, and position text as you asked
(If I properly understood your requirements :-))
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())
Updated: you could try the following variants
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)
}
}
}
there is also next interesting variant, use ascender instead of above lineHeight
.offset(x: 0, y: -self.font.ascender / 2.0)