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
Related
I have created a chart view like below. I wanted to know how to change the bar mark when user tap on that bar.
Chart {
ForEach(Data.lastOneHour, id: \.day) {
BarMark(
x: .value("Month", $0.day, unit: .hour),
y: .value("Duration", $0.duration)
)
}
}
I see .onTap modifier is not available on BarMark. And I don't see any way to access this barmark and apply color using gesture location by using Geometryreader.
You could try this approach, using a chartOverlay and a select variable to change the bar color when the user tap on that bar.
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"
var body: some View {
Chart(measurement) { data in
BarMark(x: .value("Time", data.id), y: .value("val", data.val))
.foregroundStyle(select == data.id ? .red : .blue)
}
.chartOverlay { proxy in
GeometryReader { geometry in
ZStack(alignment: .top) {
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { location in
doSelection(at: location, proxy: proxy, geometry: geometry)
}
}
}
}
}
func doSelection(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
let xPos = location.x - geometry[proxy.plotAreaFrame].origin.x
guard let xbar: String = proxy.value(atX: xPos) else { return }
select = xbar
}
}
struct Measurement: Identifiable {
var id: String
var val: Double
}
I'm trying to rearrange or move items in a LazyVGrid inside a ScrollView using .draggable and .dropDestination view modifier based on the post here.
My problem is that I need to know which item is being dragged and particularly when the user stops dragging the item, much like onEnded for DragGesture. This works fine when the item is dropped inside a view with a .dropDestination but if the user drops it outside the item get "stuck" as being the draggedItem. See the video:
Drag and Drop outside of view
Is there a way to tell when the item is dropped, regardless of where it's dropped?
Here's my code currently which only works if item is dropped within the views with .dropDestination.
import SwiftUI
import UniformTypeIdentifiers
extension UTType {
static var itemTransferable = UTType(exportedAs: "com.styrka.DragableGrid.item")
}
struct ItemDraggable: Identifiable, Equatable, Transferable, Codable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: ItemDraggable.self, contentType: .itemTransferable)
}
var id: Int
}
struct MainView: View {
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
#State private var items = (0..<20).map { ItemDraggable(id: $0) }
#State private var draggingItem: ItemDraggable?
var body: some View {
ScrollView {
Text("Items, dragging: \(draggingItem?.id ?? -1)")
LazyVGrid(columns: columns) {
ForEach(items) { item in
DraggableView(item: item, draggingItem: $draggingItem)
}
}
}
.background(Color.white)
.dropDestination(for: ItemDraggable.self) { items, location in
// User to drop items outside but does not cover the whole app
draggingItem = nil
return true
}
}
}
struct DraggableView: View {
var item: ItemDraggable
#Binding var draggingItem: ItemDraggable?
#State private var borderColor: Color = .black
#State private var borderWidth: CGFloat = 0.0
var body: some View {
Text("\(item.id)").font(.caption)
.frame(width: 100, height: 100)
.background(RoundedRectangle(cornerRadius: 16).fill(.blue).opacity(0.6))
.border(borderColor, width: borderWidth)
.opacity(item == draggingItem ? 0.1 : 1)
.draggable(item, preview: {
Text("Dragging item \(item.id)")
.onAppear {
// Set binding when draggable Preview appears..
draggingItem = item
}
.onDisappear{
// Called as soon as the dragged item leaves the 'DraggedView' frame
//draggingItem = nil
}}
)
.dropDestination(for: ItemDraggable.self) { items, location in
draggingItem = nil
return true
} isTargeted: { inDropArea in
borderColor = inDropArea ? .accentColor : .black
borderWidth = inDropArea ? 10.0 : 0.0
}
}
}
I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
I'm trying to change the row colour in the list.
struct ContentView: View {
#State var userProfiles : [Profile] = [Profile.default]
var body: some View {
VStack{
List(userProfiles.indices, id: \.self, rowContent: row(for:)).background(Color.red)
}.background(Color.red)
}
// helper function to have possibility to generate & inject proxy binding
private func row(for idx: Int) -> some View {
let isOn = Binding(
get: {
// safe getter with bounds validation
idx < self.userProfiles.count ? self.userProfiles[idx] : DtrProfile.default
},
set: { self.userProfiles[idx] = $0 }
)
return Toggle(isOn: isOn.isFollower, label: { Text("\(idx)").background(Color.red) } )
}
}
Output:-
I want to achieve:-
Can someone please explain to me how to change the full row colour. I've tried to implement by above but no results yet.
Any help would be greatly appreciated.
Thanks in advance.
Use listRowBackground
var body: some View {
VStack{
List {
ForEach(0...15, id: \.self, content: row(for:)).background(Color.red).listRowBackground(Color.red)
}
}
}
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.