Related
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()
}
}
I am trying to animate the color transition between green, yellow, and red. How would I go about this? I have tried using animation() on the Circle view, but this created weird bugs in which the StrokeStyle seems to be ignored and the whole circle fills with color during the transition.
import SwiftUI
import UserNotifications
struct CircleTimer_Previews: PreviewProvider {
static var previews: some View {
Group {
CircleTimer() }
}
}
struct CircleTimer : View {
#State var to: CGFloat = 1;
#State var start = false
#State var count = 15
let defaultTime = 15
#State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View{
ZStack{
Color.black.opacity(0.06).edgesIgnoringSafeArea(.all)
VStack{
ZStack{
Circle()
.trim(from: 0, to: 1)
.stroke(Color.black.opacity(0.09), style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
Circle()
.trim(from: 0, to: self.to)
.stroke(self.count > 10 ? Color.green : self.count > 5 ? Color.yellow : Color.red, style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
.rotationEffect(.init(degrees: -90))
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
VStack{
Text("\(self.count)")
.font(.system(size: 72))
.fontWeight(.bold)
}
}
TimerButtons(start: $start, to: $to, count: $count)
}
}
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge,.sound,.alert]) { (_, _) in
}
})
.onReceive(self.time) { (_) in
if self.start{
if self.count > 0 {
self.count -= 1
print(self.count)
withAnimation(.default){
self.to = CGFloat(self.count) / 15
print(self.to)
}
}
else{
self.start.toggle()
}
}
}
}
}
As suggested by #Yrb, you must create three separate views of three different colors.
I've got myself in a bit of a pickle. I'm learning swift via a variety of online tutorials and at the same time coding a side project as further self learning. The side project will be a VERY basic football manager style app which I figure will give me a bit of UI, data handling, and algorithm refactoring practice. It is this side project where I have hit a blocker. I understand the principle of state variables to maintain a permanent link between an app state and the UI and I also understand the general principle of using observable objects / environment and and #Published to allow those variables to be available in multiple "views" however the implementation / syntax is eluding me.
The (lengthy) code below basically sets up a home screen on the UI where two teams abilities are compared. This is done via ability bars and a radar chart. Both the ability bar and the radar chart work correctly but I can't seem to link them to the same variable. The ability bar uses a series of #State variables called in the main content.view and then passed via $bindings from a child view to its parent but the radar plot is populated by local variables within a data array. I can't work out how to put the state variables into the array, either as is, or by using observable or environmental variables. Basically I want to declare the 6 variables in one location and then use them inside the array and also inside the various views.
I'd be really appreciative if someone could offer some suggestions or indeed to point me in a different direction if I am completely on a tangent with this.
By way of a quick summary...
First I have an ability bar view which creates something similar to a progress bar...
//Ability Bar View
struct abilityBar: View {
#Binding var value: Double
var color1: Color
var color2: Color
var title: String
var barHeight: Float
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: -3) {
HStack {
Text("\(title)")
.modifier(lightFont())
Spacer()
Text("\(self.value, specifier: "%.1f")")
.modifier(heavyFont())
}
Spacer()
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4)
.frame(width: geometry.size.width, height: CGFloat(barHeight))
.foregroundColor(.white)
.opacity(0.075)
RoundedRectangle(cornerRadius: 4)
.fill(LinearGradient(
gradient: Gradient(colors: [color1, color2]),
startPoint: .leading,
endPoint: .trailing))
.frame(width: ((CGFloat(self.value)/10) * geometry.size.width), height: CGFloat(barHeight))
}
}
}
}
}
Second I have a team widget view that nests these ability bars and is passed variables by way of $Bindings
//Team Widget View
struct teamWidget: View {
#Binding var defence: Double
#Binding var midfield: Double
#Binding var attack: Double
var color1: Color
var color2: Color
var location: String
var teamName: String
var position: String
var body: some View {
ZStack {
Rectangle()
.frame(height: 185.0)
.foregroundColor(Color(red: 0.23921568627450981, green: 0.24313725490196078, blue: 0.3215686274509804))
.shadow(color: .black, radius: 4, x: 4, y: 4)
HStack {
Rectangle()
.fill(LinearGradient(
gradient: Gradient(colors: [color1, color2]),
startPoint: .top,
endPoint: .bottom))
.frame(height: 185.0)
.frame(width: 8)
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 0.0) {
Text("\(location)")
.modifier(lightFont())
Text("\(teamName)")
.modifier(heavyFont())
Text("League Position")
.modifier(lightFont())
Text("\(position)")
.modifier(heavyFont())
}
VStack(spacing: 4.0) {
abilityBar(value: $defence, color1: color1, color2: color2, title: "Defence", barHeight: 9.0)
.frame(height: 28)
abilityBar(value: $midfield, color1: color1, color2: color2, title: "Midfield", barHeight: 9.0)
.frame(height: 28)
abilityBar(value: $attack, color1: color1, color2: color2, title: "Attack", barHeight: 9.0)
.frame(height: 28)
}
}
Spacer()
}
}
}
}
These are populated in the main content view via #State variables. Also in the main content view is a data array that is used to draw a radar chart (main code in a separate .swift file. What I cant work out how to do is call the same #State variables in this array. Or whether I need to define the state variables differently (observable / environment etc). For reference in this large body of code I ahve commented 3 lines (1) - where the state variables are declared (2) where the data array for the radar chart is declared and where I currently cant get state variables or equivalent to work and (3) where the ability bars are declared (work with state variables)
Apologies for the long main / lots of code. I couldn't think of a way to cut this down further.
enum RayCase:String, CaseIterable {
case defence = "Def"
case midfield = "Mid"
case attack = "Att"
}
struct DataPoint:Identifiable {
var id = UUID()
var entrys:[RayEntry]
var color:Color
init (defence:Double, midfield:Double, attack:Double, color:Color) {
self.entrys = [
RayEntry(rayCase: .defence, value: defence),
RayEntry(rayCase: .attack, value: attack),
RayEntry(rayCase: .midfield, value: midfield),
]
self.color = color
}
}
struct ContentView: View {
// 1 THIS IS WHERE STATE VARIABLES ARE DECLARED
#State var team1Defence: Double = Double.random(in: 1.0...9.9)
#State var team1Midfield: Double = Double.random(in: 1.0...9.9)
#State var team1Attack: Double = Double.random(in: 1.0...9.9)
#State var team2Defence: Double = Double.random(in: 1.0...9.9)
#State var team2Midfield: Double = Double.random(in: 1.0...9.9)
#State var team2Attack: Double = Double.random(in: 1.0...9.9)
#State var playMatchIsVisible: Bool = false
let mainRed = Color(red: 245/255, green: 39/255, blue: 85/255)
let highlightRed = Color(red: 245/255, green: 85/255, blue: 141/255)
let mainBlue = Color(red: 62/255, green: 219/255, blue: 208/255)
let highlightBlue = Color(red: 119/255, green: 237/255, blue: 194/255)
let dimensions = [
Ray(maxVal: 10, rayCase: .attack),
Ray(maxVal: 10, rayCase: .midfield),
Ray(maxVal: 10, rayCase: .defence),
]
// 2 THIS IS THE DATA ARRAY WHERE I CANT GET STATE VARIABLES TO WORK, THE SAME 6 DECLARED VARIABLES NEED TO POPULATE THIS ARRAY
let data = [
DataPoint(defence: 5, midfield: 8, attack: 7, color: .red),
DataPoint(defence: 5, midfield: 2, attack: 9, color: .blue),
]
var body: some View {
//Background Z-Stack Container
ZStack {
LinearGradient(gradient: Gradient(colors: [(Color(red: 0.23529411764705882, green: 0.23529411764705882, blue: 0.32941176470588235)), (Color(red: 0.058823529411764705, green: 0.06666666666666667, blue: 0.12941176470588237))]), startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
//Main VStack
VStack {
//Row 1 Main HStack
HStack {
//Date / Fixture ZStack
ZStack {
Rectangle()
.frame(height: 70.0)
.foregroundColor(Color(red: 0.23921568627450981, green: 0.24313725490196078, blue: 0.3215686274509804))
.shadow(color: /*#START_MENU_TOKEN#*/.black/*#END_MENU_TOKEN#*/, radius: 4, x: 4, y: 4)
//Date Fixture HStack to ensure left alignment
HStack {
// Date / Fixture VStack to put text on top of each other (6)
VStack(alignment: .leading, spacing: 4.0) {
Text("Date")
.modifier(heavyFont())
Text("Saturday 23rd October")
.modifier(lightFont())
Text("League 2 Fixture")
.modifier(heavyFont())
}
Spacer() //Forces text to LHS of widget
}
.padding(.leading, 7)
}
.padding(.leading, 15)
.padding(.trailing, 6)
// Play Match Stack (7)
ZStack {
Rectangle()
.frame(height: 70.0)
.foregroundColor(Color(red: 0.23921568627450981, green: 0.24313725490196078, blue: 0.3215686274509804))
.shadow(color: /*#START_MENU_TOKEN#*/.black/*#END_MENU_TOKEN#*/, radius: 4, x: 4, y: 4)
// Play Match HStack to ensure correct left alignment (8)
HStack {
Button(action: {
self.playMatchIsVisible = true
}) {
Image("playFull")
Text("Play Match")
.modifier(heavyFont())
}
.alert(isPresented: $playMatchIsVisible, content: {
return Alert(title: Text("Are you sure you want to kick off?"), primaryButton: .default(Text("Yes, Let's play!")), secondaryButton: .default(Text("No, not yet.")))
})
}
}
.padding(.leading, 6)
.padding(.trailing, 15)
}
.padding(.top, 15)
//Row 2 HStack
HStack {
// 3 THIS IS WHERE STATE VARIABLES ARE CALLED TO POPULATE THE ABILITY BARS AND WORK IN THIS CONTEXT AND WHEEN PASSED AS BINDINGS INTERNALLY
teamWidget(defence: $team1Defence, midfield: $team1Midfield, attack: $team1Attack, color1: highlightRed, color2: mainRed, location: "Home", teamName: "Oxford United", position: "14")
.padding(.leading, 15)
.padding(.trailing, 6)
teamWidget(defence: $team2Defence, midfield: $team2Midfield, attack: $team2Attack, color1: highlightBlue, color2: mainBlue, location: "Away", teamName: "Luton Town", position: "3")
.padding(.leading, 6)
.padding(.trailing, 15)
}
.padding(.top, 15)
VStack {
RadarChartView(width: 370, MainColor: Color.init(white: 0.8), SubtleColor: Color.init(white: 0.6), quantity_incrementalDividers: 4, dimensions: dimensions, data: data)
}
.frame(height: 275)
.padding(.top, 15)
Spacer() //Forces all to top in main stack
HStack {
Button(action: {}) {
Image("home")
}
Spacer()
Button(action: {}) {
Image("barcode")
}
Spacer()
Button(action: {}) {
Image("squad")
}
Spacer()
Button(action: {}) {
Image("fixtures")
}
Spacer()
Button(action: {}) {
Image("settings")
}
}
.padding(.leading, 20)
.padding(.trailing, 20)
.padding(.bottom, 15)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The net result of this looks something like this... but currently the ability bars and radar charts dont have the same single truth data set.
content view screen shot
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)
Having rectangle with gradient fill and trying to animate color change.
...
Rectangle()
.fill(LinearGradient(
gradient: .init(stops: [Gradient.Stop(color: myColor, location: 0.0), Gradient.Stop(color: .blue, location: 1.0)]),
startPoint: .init(x: 0.5, y: startPoint),
endPoint: .init(x: 0.5, y: 1-startPoint)
))
.animation(Animation.easeIn(duration: 2))
...
While change of starting points is animated nicely; the color change is not animated at all and just "switches"
Any hints?
So if anyone will be struggling with that my current solution is having two gradients in ZStack and animating opacity of the top one
Example gif
Here is rough example, the code sure can be written more nicely:
///helper extension to get some random Color
extension Color {
static func random()->Color {
let r = Double.random(in: 0 ... 1)
let g = Double.random(in: 0 ... 1)
let b = Double.random(in: 0 ... 1)
return Color(red: r, green: g, blue: b)
}
}
struct AnimatableGradientView: View {
#State private var gradientA: [Color] = [.white, .red]
#State private var gradientB: [Color] = [.white, .blue]
#State private var firstPlane: Bool = true
func setGradient(gradient: [Color]) {
if firstPlane {
gradientB = gradient
}
else {
gradientA = gradient
}
firstPlane = !firstPlane
}
var body: some View {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: self.gradientA), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: self.gradientB), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
.opacity(self.firstPlane ? 0 : 1)
///this button just demonstrates the solution
Button(action:{
withAnimation(.spring()) {
self.setGradient(gradient: [Color.random(), Color.random()])
}
})
{
Text("Change gradient")
}
}
}
}
Update: *In the end I have explored several ways of animating gradient fills and summarized them here: https://izakpavel.github.io/development/2019/09/30/animating-gradients-swiftui.html *
Wanted to share one more simple option.
Two rectangles with animated solid colors on top of each other.
The top ZStack uses a gradient mask to blend them together.
struct ContentView: View {
#State var switchColor = false
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(switchColor ? Color.purple : Color.green)
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(switchColor ? Color.pink : Color.yellow)
}
.mask(LinearGradient(gradient: Gradient(colors: [.clear,.black]), startPoint: .top, endPoint: .bottom))
}
.padding()
.onTapGesture {
withAnimation(.spring()) {
switchColor.toggle()
}
}
}
}