How to pass variables functions in forEach loops - swift

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)
{
}
}

Related

How to Add Labels on a LineMark Chart in SwiftUI

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

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

Sucess notification animation extra arguments error SwiftUI

I'm getting an error when trying to do the animation:
SuccessNotificationView()
.offset(y: self.showSuccessSave ? -UIScreen.main.bounds.height/3 : -UIScreen.main.bounds.height)
.animation(.interactiveSpring(mass: 1.0, stiffness: 100.0, damping: 10, initialVelocity: 0))
where SuccessNotificationView() is:
struct SuccessNotificationView: View {
var body: some View {
Text("Success")
.padding()
.foregroundColor(Color.white)
.frame(width: UIScreen.main.bounds.width, height: 100)
.background(Color.green)
.cornerRadius(20)
}
}
The error I'm getting is: Extra arguments at positions #1, #2, #3, #4 in call
How can I fix that?
You need to pass in the the proper parameters to interactiveSpring().
.animation(
.interactiveSpring(
response: Double,
dampingFraction: Double,
blendDuration: Double
)
)
There are only two method calls for interactiveSpring(). One is with no parameters and the second is the example shown above.
I believe that you are trying to use interpolatingSpring(). In that case, use the following code snippet:
.animation(
.interpolatingSpring(
mass: 1,
stiffness: 100.0,
damping: 10,
initialVelocity: 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)
}

How to create a matrix of CAShapeLayers?

I have this code:
var triangles: [[[CAShapeLayer]]] = Array(repeating: Array(repeating: Array(repeating: 0, count: 2), count: 15), count: 15);
But it generates an "Cannot convert value of type..." compilation error.
How can I solve that? I want to access my CAShapeLayers like this:
triangles[1][2][1].fillColor = UIColor(red: 40/255, green: 73/255, blue: 80/255, alpha: 1).cgColor;
Use optionals.
var triangles: [[[CAShapeLayer?]]] = Array(repeating: Array(repeating: Array(repeating: nil, count: 2), count: 15), count: 15)
Now there's a nil instead of a 0, which is what I think you were hinting at. But every triangles[x][y][z] is now an optional type you'll have to safely unwrap.
So now you have to do something like triangles[x][y][z] = CAShapeLayer() before you do anything to that object.
Edit for correction. Thanks #OOPer
I thought about it some more, and realized I didn't really answer your question.
So you may use for loops to initialize everything (which would be a pain), or you could do something like this every time you access an index:
if triangles[x][y][z] == nil
{
triangles[x][y][z] = CAShapeLayer()
}
let bloop = triangles[x][y][z]!
bloop.fillColor = UIColor(...
Then you could pull it out into an outside method so it becomes a 1 liner. Like:
func tri(at x: Int, _ y: Int, _ z: Int) -> CAShapeLayer
{
if triangles[x][y][z] == nil
{
triangles[x][y][z] = CAShapeLayer()
}
return triangles[x][y][z]!
}
Then when using it:
tri(at: 1, 2, 1).fillColor = ...
Of course, you should pull triangles out and make it a property of the class you're in, or you can include it in the parameter list of that 1 liner method.
All that nesting makes your code hard to understand, and Array(repeating:count:) can't do what you want anyway.
func newGrid() -> [[[CAShapeLayer]]] {
func newStack() -> [CAShapeLayer] {
return (0 ..< 2).map({ _ in CAShapeLayer() })
}
func newRow() -> [[CAShapeLayer]] {
return (0 ..< 15).map({ _ in newStack() })
}
return (0 ..< 15).map({ _ in newRow() })
}
var triangles = newGrid()
You cannot use "0" as the repeating value, it will be inferred to be type [[[Int]]]. Just replace "0" with "CAShapeLayer()"