I'm having an issue with SwiftUI charts and was wondering if anyone knows of a solution or has also run into this. I've been trying to plot data for my app and the X axis does not show the dates and the Y axis only has axis marks 0-9. So, I tried to make another file instance to check what's going on. This is what I have
import Charts
import SwiftUI
struct Exercise: Identifiable {
let id = UUID()
var dateCompleted: Date
var reps: Int
}
struct ContentView: View {
private var exerciseMaxRepsArray: [Exercise] = []
init() { // init dummy data
for i in 0 ..< 20 {
let date = Calendar.current.date(byAdding: .day, value: i, to: .now) ?? .now
let rep = Int.random(in: 1...20)
exerciseMaxRepsArray.append(
Exercise(dateCompleted: date, reps: rep)
)
}
}
var body: some View {
GroupBox(label: Text("Daily Max Reps")) {
Chart(exerciseMaxRepsArray) { e in
LineMark(x: .value("Date", e.dateCompleted, unit: .day),
y: .value("Reps", e.reps)
) }
.chartYAxisLabel(position: .trailing, alignment: .center) {
Text("Reps")
}
.chartXAxisLabel(position: .bottom, alignment: .center) {
Text("Date")
}
}
}
}
}
And this is the result:
I've checked and the issue is not the frame. Has anyone else had this issue or have any clue what could be going on here?
Related
I am trying to update a List using an availableModels array inside an EnvironmentObject. For some reason it doesn't refresh my view but I have no idea why. Very confused about this so help would be much appreciated, I'm new to SwiftUI and transferring data between views. I'm pretty sure that the array is updating because when I print it out the values are correct, but the list doesn't update in the ForEach loop or in the Text fields.
This is my view with the List:
import SwiftUI
var selectedModel: String = "rdps"
struct ModelSelectView: View {
#EnvironmentObject var viewModel: WeatherData
var body: some View {
NavigationView {
List{
Group {
WeatherModelHeader()
}
if !viewModel.weatherArray.isEmpty {
ForEach(viewModel.weatherArray[0].availableModels, id: \.apiName) { model in
WeatherModelCell(weatherModel: model)
}
} else {
Text("No Weather 4 u")
}
Button(action: {
fetchModelInventory(for: viewModel)
}, label: {
Text("Fetch Inventory")
})
Text(String(viewModel.weatherArray[0].availableModels[0].apiName))
Text(String(viewModel.weatherArray[0].availableModels[1].apiName))
Text(String(viewModel.weatherArray[0].availableModels[2].apiName))
Text(String(viewModel.weatherArray[0].availableModels[3].apiName))
}
.navigationTitle("Models")
.onAppear(perform: {
fetchModelInventory(for: viewModel)
print("viewModel.weatherArray.availableModels \(viewModel.weatherArray[0].availableModels)")
})
}
}
}
//Layout for the header of the list
struct WeatherModelHeader: View {
var body: some View {
HStack {
Text("Model \nName")
.bold()
.frame(width: 60, alignment: .leading)
Text("Range")
.bold()
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
Text("Resolution")
.bold()
.frame(width: 85, alignment: .leading)
.padding(.leading, 30)
}
}
}
//create the layout for the weather model list
struct WeatherModelCell: View {
let weatherModel: WeatherModel
#EnvironmentObject var viewModel: WeatherData
var body: some View {
HStack {
//need to make navlink go to correct graph model. This will be passed in the GraphViews(). Clicking this nav link will trigger the API Call for the coordinates and model.
NavigationLink(
destination: InteractiveChart()
.onAppear{
selectedModel = weatherModel.apiName
fetchData(for: viewModel)
},
label: {
Text(weatherModel.name)
.frame(width: 60, alignment: .leading)
Text("\(String(weatherModel.range)) days")
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
Text("\(String(weatherModel.resolution)) km")
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
//this triggers the api call when the model is tapped. it passes the selected model name to the variable selectedModel for the call.
})
}
}
}
And these are my models where I set up my Observable Object:
class WeatherForecastClass: ObservableObject {
//var id = UUID()
var chartMODEL: String = "Model"
var chartModelHeight: Int = 0
var chartLAT: Double = selectedCords.latitude
var chartLON: Double = selectedCords.longitude
var chartDATETIME: Date = formatDate(date: "2023-02-10 18:00")
var chartTMP: Int = 1
var chartRH: Int = 0
var chartDP: Float = 0.0
var chartTTLSnow: Float = 0.0
var chartTTLRain: Float = 0.0
var chartWindSpeed: Int = 0
var chartWindDirDegs: Int = 0
var chartWindDirString: String = ""
var availableModels: [WeatherModel] = weatherModels
}
class WeatherData: ObservableObject {
/// weather data. This is the master class.
#Published var weatherArray: [WeatherForecastClass] = [WeatherForecastClass()]
//#Published var weatherProperties: WeatherForecastClass = WeatherForecastClass()
}
And this is my function for updating the availableModels array:
import Foundation
import SwiftUI
var modelInventory: [String] = []
func fetchModelInventory(for weatherData: WeatherData) {
// #EnvironmentObject var viewModel: WeatherData
let lat = Float(selectedCords.latitude)
let lon = Float(selectedCords.longitude)
let key = "my apiKey"
//this is the url with the API call. it has the data for the call request and my API key.
let urlString = "https://spotwx.io/api.php?key=\(key)&lat=\(lat)&lon=\(lon)&model=inventory"
guard let url = URL(string: urlString) else {
return
}
print(url)
//Gets the data from the api call. tasks are async
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
print("Error")
return
}
//clear the model inventory
modelInventory = []
weatherData.weatherArray[0].availableModels = []
//decode the modelinventory csv and fill the array
if let csvString = String(data: data, encoding: .utf8) {
let lines = csvString.split(separator: "\n")
for line in lines {
let columns = line.split(separator: ",")
for column in columns {
let value = String(column)
modelInventory.append(value)
}
}
}
print("Model Inventory -----")
print(modelInventory)
if !modelInventory.isEmpty {
DispatchQueue.main.async {
for model in weatherModels {
if modelInventory.contains(model.apiName) {
weatherData.weatherArray[0].availableModels.append(model)
}
}
print(weatherData.weatherArray[0].availableModels)
}
} else {
return
}
}
task.resume()
}
I feel like I've tried everything but I no matter what the list wont update.
I'm trying to implement a way to detect hovering gesture on Chart in SwiftUI.
At first I tried adding .onHover() modifier to BarMark like below, but it seems it doesn't work because BarMark does not conform to View Protocol while .onHover() is defined View.
Chart(modules) { module in
BarMark(x: .value("name", module.name), y: .value("Score", module.score)).onHover { over in
details = module.name
} // gives error
}
So does that mean If I want to show additional data when user hover over the Chart graph, then I have to create my own Chart graph rather than using Chart from Charts framework?
You could try this approach, using an .chartOverlay and .onContinuousHover.
You will have to adjust the location calculations to suit your purpose.
struct ContentView: View {
let measurement: [Measurement] = [
Measurement(id: "1", val: 11.2),
Measurement(id: "2", val: 22.2),
Measurement(id: "3", val: 38.2)
]
#State var select = "0"
#State var isHovering = false
var body: some View {
Chart {
ForEach(measurement) { data in
BarMark(x: .value("Time", data.id), y: .value("val", data.val))
.foregroundStyle(select == data.id ? .blue : .red)
}
}
.chartOverlay { proxy in
GeometryReader { geometry in
ZStack(alignment: .top) {
Rectangle().fill(.clear).contentShape(Rectangle())
.onContinuousHover { phase in
switch phase {
case .active(let location):
bar(at: location, proxy: proxy, geometry: geometry)
isHovering = true
case .ended:
isHovering = false
}
}
}
}
}
}
func bar(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
let yPosition = location.y - geometry[proxy.plotAreaFrame].origin.y
guard let month: String = proxy.value(atX: xPosition) else { return }
guard let measure: Double = proxy.value(atY: yPosition) else { return }
// more logic here ....
select = month
}
}
struct Measurement: Identifiable {
var id: String
var val: Double
}
If you want to just tap on the BarMarks, see my other answer at: How to change the color of BarView in SwiftUI charts when we tap on it
I have modified a custom 5 star rating view (https://swiftuirecipes.com/blog/star-rating-view-in-swiftui) to suite my needs. I use that view in several places in my app to display the current rating for a struct and to change the rating through a selectable list. The problem I have is that when I select a new value for the star rating through the NavigationLink, the underlying rating value changes, but the display does not. I have created a sample app that shows the problem and included it below.
//
// StarTestApp.swift
// StarTest
//
// Created by Ferdinand Rios on 12/20/21.
//
import SwiftUI
#main
struct StarTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct StarHolder {
var rating: Double = 3.5
}
struct ContentView: View {
#State private var starHolder = StarHolder()
var body: some View {
NavigationView {
NavigationLink {
RatingView(starHolder: $starHolder)
} label: {
HStack {
Text("Rating: \(starHolder.rating, specifier: "%.1f")")
Spacer()
StarRatingView(rating: starHolder.rating, fontSize: 15)
}
.padding()
}
.navigationTitle("Test")
}
}
}
struct RatingView: View {
#Binding var starHolder: StarHolder
var body: some View {
List {
ForEach(0..<11, id: \.self) { index in
HStack {
StarRatingView(rating: Double(index) * 0.5, fontSize: 15)
Spacer()
Image(systemName: starHolder.rating == Double(index) * 0.5 ? "checkmark" : "")
}
.contentShape(Rectangle()) //allows to tap whole area
.onTapGesture {
starHolder.rating = Double(index) * 0.5
}
}
}
.navigationBarTitle(Text("Rating"))
}
}
struct StarRatingView: View {
private static let MAX_RATING: Double = 5 // Defines upper limit of the rating
private let RATING_COLOR = Color(UIColor(red: 1.0, green: 0.714, blue: 0.0, alpha: 1.0))
private let EMPTY_COLOR = Color(UIColor.lightGray)
private let fullCount: Int
private let emptyCount: Int
private let halfFullCount: Int
let rating: Double
let fontSize: Double
init(rating: Double, fontSize: Double) {
self.rating = rating
self.fontSize = fontSize
fullCount = Int(rating)
emptyCount = Int(StarRatingView.MAX_RATING - rating)
halfFullCount = (Double(fullCount + emptyCount) < StarRatingView.MAX_RATING) ? 1 : 0
}
var body: some View {
HStack (spacing: 0.5) {
ForEach(0..<fullCount) { _ in
self.fullStar
}
ForEach(0..<halfFullCount) { _ in
self.halfFullStar
}
ForEach(0..<emptyCount) { _ in
self.emptyStar
}
}
}
private var fullStar: some View {
Image(systemName: "star.fill").foregroundColor(RATING_COLOR)
.font(.system(size: fontSize))
}
private var halfFullStar: some View {
Image(systemName: "star.lefthalf.fill").foregroundColor(RATING_COLOR)
.font(.system(size: fontSize))
}
private var emptyStar: some View {
Image(systemName: "star").foregroundColor(EMPTY_COLOR)
.font(.system(size: fontSize))
}
}
If you run the app, the initial rating will be 3.5 and the stars will show the correct rating. When you select the stars, the RatingView will display with the correct rating checked. Select another rating and return to the ContentView. The text for the rating will update, but the star rating will still be the same as before.
Can anyone point me to what I am doing wrong here? I assume that the StarRatingView would refresh when the starHolder rating changes.
There are a couple of problems here. First, in your RatingView, you are passing a Binding<StarHolder>, but when you update the rating, the struct doesn't show as changed. To fix this, pass in a Binding<Double>, and the change will get noted in ContentView.
The second problem is that StarRatingView can't pick up on the change, so it needs some help. I just stuck an .id(starHolder.rating) onto StarRatingView in ContentView, and that signals to SwiftUI when the StarRatingView has changed so it is updated.
struct ContentView: View {
#State private var starHolder = StarHolder()
var body: some View {
NavigationView {
VStack {
NavigationLink {
RatingView(rating: $starHolder.rating)
} label: {
HStack {
Text("Rating: \(starHolder.rating, specifier: "%.1f")")
Spacer()
StarRatingView(rating: starHolder.rating, fontSize: 15)
.id(starHolder.rating)
}
.padding()
}
.navigationTitle("Test")
}
}
}
}
struct RatingView: View {
#Binding var rating: Double
var body: some View {
List {
ForEach(0..<11, id: \.self) { index in
HStack {
StarRatingView(rating: Double(index) * 0.5, fontSize: 15)
Spacer()
Image(systemName: rating == Double(index) * 0.5 ? "circle.fill" : "circle")
}
.contentShape(Rectangle()) //allows to tap whole area
.onTapGesture {
rating = Double(index) * 0.5
}
}
}
.navigationBarTitle(Text("Rating"))
}
}
One last thing. SwiftUI does not like the "" as an SF Symbol, so I changed the checks to "circle" and "circle.fill". Regardless, you should provide an actual image for both parts of the ternary. Or you could use a "check" and make .opacity() the ternary.
In your StarRatingView change the ForEach(0..<fullCount) {...} etc...
to ForEach(0..<fullCount, id: \.self) {...}.
Same for halfFullCount and emptyCount.
Works well for me.
I have a button which add a new object of Type CircleInfo to an array called circleArray! this array will be used to supply data to work for a ForEach loop, All is working except one big issue that I noticed!
The Issue: When I add new object to array, the action of updating array make ForEach to update itself as well, and it brings app and Memory to knee with a little more detailed View than a simple Circle, because it starts Re-Initializing Initialized-View again! For example we got 10 Circle in Screen, as soon as I add new Circle, Foreach will go render all rendered 10 circle plus the new one!
Maybe you say it is the way of working SwiftUI which draw screen with small changes, But I am telling with more and more data it goes eat so much memory and CPU!
My Goal: I want to find a new way of adding new object to screen, more likely adding overly or something like that which those not need ForEach! because as soon as we change items of array or range of that array, it will go draw all again!
struct CircleInfo: Identifiable {
let id: UUID = UUID()
let circleColor: Color = Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1))
}
struct CircleView: View {
let circleInfo: CircleInfo
let index: Int
init(circleInfo: CircleInfo, index: Int) {
self.circleInfo = circleInfo
self.index = index
print("initializing circle:", index)
}
var body: some View {
Circle()
.fill(circleInfo.circleColor)
.frame(width: 200, height: 200, alignment: .center)
}
}
struct ContentView: View {
#State var circleArray: [CircleInfo] = [CircleInfo]()
var body: some View {
GeometryReader { geometry in
Color
.yellow
.ignoresSafeArea()
ForEach(Array(circleArray.enumerated()), id:\.element.id) { (index, item) in
CircleView(circleInfo: item, index: index)
.position(x: geometry.size.width/2, y: 20*CGFloat(index) + 100)
}
}
.overlay(button, alignment: Alignment.bottom)
}
var button: some View {
Button("add Circle") { circleArray.append(CircleInfo()) }
}
}
Easiest way to achieve your goal is to have an array of CircleView instead of CircleInfo.
First make CircleView identifiable:
struct CircleView: View, Identifiable {
let id: UUID = UUID()
…
Then use a CircleView array in ForEach
struct ContentView: View {
#State var circleArray: [CircleView] = [CircleView]()
var body: some View {
GeometryReader { geometry in
Color
.yellow
.ignoresSafeArea()
ForEach(circleArray) { item in
item
.position(x: geometry.size.width/2, y: 20*CGFloat(item.index) + 100)
}
}
.overlay(button, alignment: Alignment.bottom)
}
var button: some View {
Button("add Circle") { circleArray.append(CircleView(circleInfo: CircleInfo(), index: circleArray.count)) }
}
}
You'll definitely use the CPU less.
I am using SwiftUI but have a conceptual problem.
I have edited down my code for (hopefully to fully explain the issue )
I want to do some calculations after the button is pressed AFTER which I want to plot the graph.
I am using minX as the bogus result of calculations.
Firstly, note that I have a file (called myGlobal.swift) the contents of which are:
import Foundation
class myGlobalVariables: ObservableObject
{
#Published var minX :Double = 99999
}
The value of this in the getLineChartDataSet = 9999
(from the Global file)
Before I press the button
This changed after I press the button to 222.222 in Mohrs2DCalc func
But of course I have already run myLineChartSwiftUI before I pressed the button,
But want to run myLineChartSwiftUI only AFTER I have pressed the button
import SwiftUI
import Charts
struct PricipalStresses2D: View {
#ObservedObject var input = myGlobalVariables()
var body: some View
{
ScrollView(.vertical, showsIndicators: true)
{
VStack(spacing: -10)
{
Button(action: {
self.CheckInputs()
})
{
Text("Calculations Before Plot")
}.padding(50)
myLineChartSwiftUI() //use frame to change the graph size within your SwiftUI view
.frame(alignment: .center)
.aspectRatio(contentMode: .fit)
}
}
}
func CheckInputs()
{
//Assume all inputs are OK
self.Mohrs2DCalc()
}
func Mohrs2DCalc()
{
let minX = 222.222 // Just to set it as if we had calculated it
self.input.minX = minX
print("Mohrs circle minX = \(self.input.minX)")
}
struct myLineChartSwiftUI : UIViewRepresentable
{
let lineChart = LineChartView()
func makeUIView(context: UIViewRepresentableContext<myLineChartSwiftUI>) -> LineChartView {
setUpChart()
return lineChart
}
func updateUIView(_ uiView: LineChartView, context: UIViewRepresentableContext<myLineChartSwiftUI>) {
}
func setUpChart() {
//lineChart.noDataText = "No Data Available"
let dataSets = [getLineChartDataSet()]
let data = LineChartData(dataSets: dataSets)
//data.setValueFont(.systemFont(ofSize: 7, weight: .light))
lineChart.data = data
}
func getChartDataPoints(sessions: [Int], accuracy: [Double]) -> [ChartDataEntry] {
var dataPoints: [ChartDataEntry] = []
for count in (0..<sessions.count) {
dataPoints.append(ChartDataEntry.init(x: Double(sessions[count]), y: accuracy[count]))
}
return dataPoints
}
func getLineChartDataSet() -> LineChartDataSet {
print("In getLineChartDataSet, minX = ", myGlobalVariables.self.init().minX) // This works fine
let dataPoints = getChartDataPoints(sessions: [0,1], accuracy: [0.0,10.0]) // sessions = x, accuracy = y
let set = LineChartDataSet(entries: dataPoints, label: "DataSet")
set.lineWidth = 0
//set.circleRadius = 4
//set.circleHoleRadius = 2
let color = ChartColorTemplates.vordiplom()[4]
set.setColor(color)
//set.setCircleColor(color)
// Got to set the xMin and xMax from the global to obtain it here
return set
}
}
} //View
struct PricipalStresses2D_Previews: PreviewProvider {
static var previews: some View {
PricipalStresses2D()
}
}
Your help will be appreciated,
TIA, Phil.