How to Add Labels on a LineMark Chart in SwiftUI - swift

I'm creating a chart in SwiftUI. It's weather related and I have the data successfully displaying. Now, I'm attempting to display the temperature on each symbol in the chart, but it only displays on the first symbol.
Here's what I've done:
Chart {
ForEach(temperatures, id: \.tempType) { series in
ForEach(series.data) { element in
LineMark(x: .value("Day", element.day), y: .value("Temps", element.temp))
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("TemperatureType", series.tempType))
.symbol {
Circle()
.fill(.yellow)
.frame(width: 10)
.shadow(radius: 2)
}
.lineStyle(.init(lineWidth: 5))
} // end for each
} // end for each
}
This works. Then, I attempt to add text using this modifier on the LineMark:
.annotation(position: .overlay, alignment: .bottom) {
let text = "\(element.temp)"
Text(text)
}
It only displays the text on the first symbol's data point:
Since the annotation modifier is within the ForEach loop, I thought it would display the temperature at each data point, but it doesn't. What's the best way to have the temperature displayed at each symbol instead of only the first?

The short answer is that the .annotation refers to the type of "Mark" that you attach it to - and you are attaching it to a LineMark, so it is the entire line you are "annotating", not the individual points.
Had you used BarMarks or PointMarks, the annotation will attach to the individual bar or point. So try this instead:
Chart {
ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
LineMark(
x: .value("Index", index),
y: .value("Value", number)
)
.lineStyle(.init(lineWidth: 5))
PointMark(
x: .value("Index", index),
y: .value("Value", number)
)
.annotation(position: .overlay,
alignment: .bottom,
spacing: 10) {
Text("\(number)")
.font(.largeTitle)
}
}
}
To make it compatible with your nice symbols, we need to add a couple of extra steps:
Chart {
ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
LineMark(
x: .value("Index", index),
y: .value("Value", number)
)
.interpolationMethod(.catmullRom)
.lineStyle(.init(lineWidth: 5))
.symbol {
// This still needs to be associated
// with the LineMark
Circle()
.fill(.yellow)
.frame(width: 10)
.shadow(radius: 2)
}
PointMark(
x: .value("Index", index),
y: .value("Value", number)
)
// We need .opacity(0) or it will
// overlay your `.symbol`
.opacity(0)
.annotation(position: .overlay,
alignment: .bottom,
spacing: 10) {
Text("\(number)")
.font(.largeTitle)
}
}
}

Related

SwiftUI Charts how to add a indication line to the selected (or last) BarMark

I'm trying to create a Range chart that looks like a native iOS heart rate chart but with some additional lines on it. . I found the example here https://github.com/jordibruin/Swift-Charts-Examples
I tried to modify my chart by adding a GeometryReader from another chart. FindElement works fine but nothing happens after. So maybe you know a fast answer what should I fix or add to show that line? Or maybe can take a piece of advice on how to get it done using another solution.
private var chart: some View {
Chart(data, id: \.weekday) { dataPoint in
Plot {
BarMark(
x: .value("Day", dataPoint.weekday, unit: .day),
yStart: .value("BPM Min", dataPoint.dailyMin),
yEnd: .value("BPM Max", dataPoint.dailyMax),
width: .fixed(isOverview ? 8 : barWidth)
)
.clipShape(Capsule())
.foregroundStyle(chartColor.gradient)
}
.accessibilityLabel(dataPoint.weekday.weekdayString)
.accessibilityValue("\(dataPoint.dailyMin) to \(dataPoint.dailyMax) BPM")
.accessibilityHidden(isOverview)
}
.chartOverlay { proxy in
GeometryReader { geo in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(
SpatialTapGesture()
.onEnded { value in
let element = findElement(location: value.location, proxy: proxy, geometry: geo)
if selectedElement?.weekday == element?.weekday {
// If tapping the same element, clear the selection.
selectedElement = nil
} else {
selectedElement = element
}
}
.exclusively(
before: DragGesture()
.onChanged { value in
selectedElement = findElement(
location: value.location,
proxy: proxy,
geometry: geo
)
}
)
)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: ChartStrideBy.day.time)) { _ in
AxisTick().foregroundStyle(.white)
}
}
.accessibilityChartDescriptor(self)
.chartYAxis(.visible)
.chartXAxis(.visible)
.frame(height: isOverview ? Constants.previewChartHeight : Constants.detailChartHeight)
}
Just found that a better solution will be adding another MarkBar with different color and width at the last element to create that vertical line. Also to create that dashed line with annotation it's better to use RuleMark.
So the final code will look like that(data is var data = HeartRateData.lastWeek, you can get it from Apple charts examples code):
private var chart: some View {
Chart(data, id: \.day) { dataPoint in
Plot {
BarMark(
x: .value("Day", dataPoint.day, unit: .day),
yStart: .value("BPM Min", dataPoint.dailyMin),
yEnd: .value("BPM Max", dataPoint.dailyMax),
width: .fixed(6)
)
.clipShape(Capsule())
.foregroundStyle(chartColor.gradient)
}
.accessibilityLabel(dataPoint.day.weekdayString)
.accessibilityValue("\(dataPoint.dailyMin) to \(dataPoint.dailyMax) BPM")
RuleMark(y: .value("min", 50))
.foregroundStyle(.gray)
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
.annotation(position: .trailing, alignment: .leading) {
Text("50")
.foregroundStyle(.gray)
.offset(x: 10, y: 0)
}
RuleMark(y: .value("max", 90))
.foregroundStyle(.gray)
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
.annotation(position: .trailing, alignment: .leading) {
Text("90")
.foregroundStyle(.gray)
.offset(x: 10, y: 0)
}
BarMark(
x: .value("Day", data.last!.day, unit: .day),
yStart: .value("BPM Min", 40),
yEnd: .value("BPM Max", 100),
width: .fixed(2)
)
.clipShape(Capsule())
.foregroundStyle(.white)
}
.accessibilityChartDescriptor(self)
.chartYAxis(.hidden)
.chartXAxis(.hidden)
.frame(height: 64)
}

SwiftUi what is the best way to number rows once you have a model

I have a view that gets data from an API what I want to do is rank rows based on points from highest to lowest (I am able to do that now) however issue is that I want to show row number like #1, #2 etc .. as you can see from below image in the red background it says 1 2 for both rows instead of the first saying 1 and the second row saying 2 . This is my code below . I have put a comment of // issue is here which is were the problem lies . As you can see I'm already using a forEach that passes in api data . Is there a way I can use that foreach ForEach(result) and number the rows from 1...n and still be able to retrieve the data like I'm doing . I have seen examples of people doing that but it is usually for a simple string array . Any suggestions would be great .
ScrollView(showsIndicators: false) {
LazyVStack {
let result = model.sorted {
$0.points! > $1.points!
}
ForEach(result) { value in
VStack(alignment: .leading) {
Spacer()
HStack {
ForEach(0..<result.count) { i in
Text("\(i + 1)")
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(.white)
.background(Color.red)
} // issue is here
Text(value.initials!)
.fontWeight(.bold)
.frame(width: 50, height: 50, alignment: .center)
.foregroundColor(Color(SystemColorPicked(picked: value.color)))
.padding()
.overlay(
Circle()
.stroke(Color.white, lineWidth: 4)
.padding(6)
)
VStack(alignment: .leading, spacing: 1) {
Text("\(value.fullname ?? "NA")")
.foregroundColor(.white)
.fontWeight(.bold)
Text("\(value.points ?? 0) | points")
.foregroundColor(.white)
}
}
}
}
}
}
The problem is you have a ForEach running within in item of your other ForEach. The second one is the one that is displaying "1 2" each time, since it iterates through all the results every time.
I don't have all of your code, so I'm not sure what type results is, but here's a sample that you can adapt to your use case. Keep in mind you'll be doing this to the outer (ie first) ForEach loop:
struct ContentView : View {
#State private var results = ["Test1","Test2","Test3"]
var body: some View {
VStack {
ForEach(Array(results.enumerated()), id: \.1) { (index, value) in
Text("\(index + 1) \(value)")
}
}
}
}
What's going on:
For the ForEach, I'm using results (or, result in your code) .enumerated -- this gives an index/item pair. I'm wrapping it in Array() because otherwise the ForEach won't want to take that specific type of sequence.
ForEach has to have an id -- here I'm just using the String itself. For your purpose, it might be different. .1 refers to the second item of the tuple (ie the item, not the index)
In the ForEach closure, you now get index and value

Can I improve Array filter in Swift?

I make some search bar in launching app.
I want to find some string that query contains regardless of lowercase and uppercase.
For Example when I search "par", "paris","parissss","safaParis" will appear.
I have a number of 5632 search item. so My code is this
List {
HStack {
TextField("Insert yout city", text: $searchTerm)
Spacer()
if self.searchTerm != "" {
Button(action:{
self.searchTerm=""
})
{
Image(systemName:"xmark.circle.fill")
.foregroundColor(Color.gray)
} .padding(.trailing, 15)
}
} .padding(10)
.background(RoundedRectangle(cornerRadius: 10)
// .strokeBorder(Color("a1"), lineWidth: 3))
.strokeBorder( LinearGradient(gradient: .init(colors: [Color("a3"),Color("a2")]),
startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 3))
if self.searchTerm.count > 1 {
ForEach((self.cities.filter { (value:String)->Bool in
return value.lowercased().contains(self.searchTerm.lowercased())
}),
id: \.self) { city in
CityRow(city: city,selectedCity: self.$selectedCity,isshow:self.$showSearchView, poiListData: self.$POIinCity)
}
}
}
}.padding(.top)
.background(Color.white.edgesIgnoringSafeArea(.all))
But I'm dissapointed at this performance. About 2~3 seconds, thread is stopped.
when I delete lowercase sentence in that code, it work well.
How can I acheive good performance about Array filter??

How to pass variables functions in forEach loops

Goal:
I am currently working on some code to create a grid of hexagons, each one individually coloured with a gradient. However I am quite new to swiftUI and don't fully understand why my code doesn't work.
Code:
ForEach(0 ..< 11, id: \.self) { Xnum in
ForEach(0 ..< 11, id: \.self) { Ynum in
self.MyNewPath(in: CGRect(), Xcoord: Double(Xnum), Ycoord: Double(Ynum), Type: 1)
.fill(RadialGradient(
gradient: Gradient(colors: [Color.red, Color.blue]),
center: .init(
x: self.gradientParametersX(Xpos: Double(Xnum)),
y: self.gradientParametersY(Ypos: Double(Ynum), Xpos: Double(Xnum))),
startRadius: CGFloat(2.0),
endRadius: CGFloat(70.0)))
.gesture(
TapGesture().onEnded {_ in
self.dataEditor(Xplace: Int(Xnum), Yplace: Int(Ynum))
}
)
}
}
Return Types:
MyNewPath() - Path (of a hexagon)
gradientParametersY() - CGFloat
gradientParametersY() - CGFloat
Error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
The error appears to be centred around the passing of Xnum and Ynum to functions gradientParametersX() and gradientParametersY().
When I pass 7.0 and 0.0 for example:
x: self.gradientParametersX(Xpos: Double(7.0)),
y: self.gradientParametersY(Ypos: Double(0.0), Xpos: Double(7.0))),
...there is no error.
Try to separate grid item creation in separate function as below (the code snapshot is not testable, so just scratchy to demo idea)
...
ForEach(0 ..< 11, id: \.self) { Xnum in
ForEach(0 ..< 11, id: \.self) { Ynum in
self.createItem(for: Xnum, Ynum: Ynum)
}
}
...
private func createItem(for Xnum: Int, Ynum: Int) -> some View {
self.MyNewPath(in: CGRect(), Xcoord: Double(Xnum), Ycoord: Double(Ynum), Type: 1)
.fill(RadialGradient(
gradient: Gradient(colors: [Color.red, Color.blue]),
center: .init(
x: self.gradientParametersX(Xpos: Double(Xnum)),
y: self.gradientParametersY(Ypos: Double(Ynum), Xpos: Double(Xnum))),
startRadius: CGFloat(2.0),
endRadius: CGFloat(70.0)))
.gesture(
TapGesture().onEnded {_ in
self.dataEditor(Xplace: Int(Xnum), Yplace: Int(Ynum))
}
)
}
EDIT : Does your answer concerns SwiftUI and not just Swift ? If yes, please edit your question, and see user3441734 comment under it.
The compilator seems to have troubles deducing the types of Xnum and Ynum.
You might have to explicitly write their type.
Your ForEach syntax seems quite weird, have you tried
for Xnum:Int in 0..<11
{
for Ynum:Int in 0..<11
{
}
}
And then casting Xnum and Ynum to Double ?
You can also directly do a foreach on doubles this way :
for Xnum:Double in stride(from: 0, through: 10, by: 1.0)
{
for Ynum:Double in stride(from: 0, through: 10, by: 1.0)
{
}
}

Breaking up the expression into distinct sub-expressions in a ForEach/ZStack (SwiftUI)

I have a little problem on the lastest SwiftUI, the error is "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
My code is like that :
let cards = ["Azertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop", "Hzertyuiop", "Izertyuiop", "Jzertyuiop", "Kzertyuiop", "Lzertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop", "Lzertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop", "Lzertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop"]
var body: some View {
ScrollView{
VStack (spacing: 0, content: {
ForEach(0..<cards.count/3) { row in // create number of rows
HStack (spacing: 0, content: {
ForEach(0..<3) { column in // create 3 columns
ZStack(alignment: .bottomLeading){
Image("ghost").resizable().aspectRatio(contentMode: .fill)
.overlay(Rectangle().fill (LinearGradient(gradient: Gradient(colors: [.clear, .black]),startPoint: .center, endPoint: .bottom)).clipped())
Text(self.cards[row * 3 + column]) // this cause the error
.fontWeight(.semibold)
}
}
})
}
})
}
}
I guess that the error comes from : row * 3 + column
So I tried to put the integer 1 instead of this calculation, and it worked.
How to do this calculation in my body and my View? because SwiftUI does not allow me and shows me "Expected pattern"
Thanks a lot !
There are two problems. One is using ZStack with overlay, the other is Linear Gradient is a view and you can use it directly.
ZStack(alignment: .bottomLeading){
Image("ghost").resizable().aspectRatio(contentMode: .fill)
LinearGradient(gradient: Gradient(colors: [.clear, .black]),startPoint: .center, endPoint: .bottom).clipped()
Text(self.cards[row * 3 + column]) // this cause the error
.fontWeight(.semibold)
}