I am using iOS- Charts, and i created bar chart. I also conformed the delegate so that i can get callbacks when i am tapped on a bar but the problem is when i tapped in the non bar area also the delegate method is getting called and getting highlighted. I am using Swift3.
var mMonths : [String]?
var mAverage : Double?
var mValues : [Double]?
override func viewDidLoad() {
super.viewDidLoad()
mMonths = ["A","B","C","D","F","G","H","I","J","K","L","M"]
}
override func viewWillAppear(_ animated: Bool) {
mAverage = 50
mValues = [20,40.0,0,50,10,100,55,80,40,10.50,80,35]
mBarChartView.delegate = self
setChart(dataPoints: mMonths!, values: mValues!)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// For setting up the data and customizing the Chart
private func setChart(dataPoints :[String] , values : [Double])
{
var dataEntries : [BarChartDataEntry] = []
var colors : [UIColor] = []
let belowAverageColor : UIColor = UIColor.blue
let aboveAverageColor : UIColor = UIColor.blue.withAlphaComponent(0.5)
let averageColor : UIColor = UIColor.lightGray
for i in 0..<dataPoints.count
{
let eachValue : Double = values[i]
var entry : BarChartDataEntry?
if eachValue == 0
{
entry = BarChartDataEntry.init(x: Double(i), yValues: [mAverage!])
colors.append(averageColor)
}
else if eachValue <= mAverage!
{
entry = BarChartDataEntry.init(x: Double(i), yValues: [eachValue,mAverage!-eachValue])
colors.append(belowAverageColor)
colors.append(averageColor)
}
else
{
entry = BarChartDataEntry.init(x: Double(i), yValues: [mAverage!,eachValue-mAverage!])
colors.append(belowAverageColor)
colors.append(aboveAverageColor)
}
dataEntries.append(entry!)
}
let dataSet = BarChartDataSet.init(values: dataEntries, label: "")
// removed value on top of each bar
dataSet.drawValuesEnabled = false
// removing the highlight on bar tapped
dataSet.highlightAlpha = 0
// assigning colors to bar
dataSet.colors = colors
let data = BarChartData(dataSet: dataSet)
mBarChartView.data = data
// Skipping labels in between
mBarChartView.xAxis.setLabelCount(mMonths!.count, force: false)
// setting data on X axis
mBarChartView.xAxis.valueFormatter = IndexAxisValueFormatter.init(values: mMonths!)
// color of labels on xaxis
mBarChartView.xAxis.labelTextColor = UIColor.black
// setting maximum value of the graph
mBarChartView.leftAxis.axisMaximum = 100
mBarChartView.rightAxis.axisMaximum = 100
mBarChartView.leftAxis.axisMinimum = 0.0
mBarChartView.rightAxis.axisMinimum = 0.0
// removing grid lines
mBarChartView.leftAxis.drawGridLinesEnabled = false
mBarChartView.rightAxis.drawGridLinesEnabled = false
mBarChartView.xAxis.drawGridLinesEnabled = false
// removing left and right axis
mBarChartView.leftAxis.enabled = false
mBarChartView.rightAxis.enabled = false
// removing bottom line
mBarChartView.xAxis.drawAxisLineEnabled = false
// Emptying the description label
mBarChartView.chartDescription?.text = ""
// placing the X axis label to bottom
mBarChartView.xAxis.labelPosition = .bottom
// bottom information about the bars is hidden
mBarChartView.legend.enabled = false
// Disabling the Zooming
mBarChartView.doubleTapToZoomEnabled = false
mBarChartView.pinchZoomEnabled = true
mBarChartView.scaleXEnabled = true
mBarChartView.scaleYEnabled = false
mBarChartView.highlightFullBarEnabled = true
}
Thank you
check this image
In the ChartHighlighter.swift file you can change the closestSelectionDetailByPixel function to this:
internal func closestSelectionDetailByPixel(
closestValues: [Highlight],
x: CGFloat,
y: CGFloat,
axis: YAxis.AxisDependency?,
minSelectionDistance: CGFloat) -> Highlight?
{
var distance = minSelectionDistance
var closest: Highlight?
for i in 0 ..< closestValues.count
{
let high = closestValues[i]
if axis == nil || high.axis == axis
{
print("high.xPx: \(high.xPx)")
print("high.x: \(high.x)")
print("high.yPx: \(high.yPx)")
print("high.y: \(high.y)")
print("x: \(x)")
print("y: \(y)")
let cDistance = getDistance(x1: x, y1: y, x2: high.xPx, y2: high.yPx)
print("cDistance: \(cDistance)")
if cDistance < distance
{
closest = high
distance = cDistance
//if the y position where user clicks is above the value, return nil
if(y<high.yPx) {
print("returning nil")
return nil
}
}
}
}
print("closest: \(closest)")
return closest
}
The only thing I added was this:
//if the y position where user clicks is above the value, return nil
if(y<high.yPx) {
print("returning nil")
return nil
}
and the print statements, if you need to do more changes you can use them to understand better how the function works. Otherwise just delete the print statements.
Related
I am trying to build a graph with 2 LineChartDataSet. At the first time, I build with one and then on every selected value, I want to do some different color to the right data set yet it seems like the last set sort of run over the settings and do it the opposite:
class GraphTableViewCell: UITableViewCell {
#IBOutlet weak var yieldLabel: UILabel!
#IBOutlet weak var yieldPercentLabel: UILabel!
#IBOutlet weak var lineChart: LineChartView!
#IBOutlet weak var graphButtonView: AssetGraphButtonView!
#IBOutlet weak var endDateLabel: UILabel!
#IBOutlet weak var startDateLabel: UILabel!
var selectionView: AssetGraphSelectionView!
var viewModel: GraphViewModelType!
var set: LineChartDataSet!
var set1: LineChartDataSet!
var marker = BalloonMarker(color: .red,
font: UIFont.systemFont(ofSize: 15),
textColor: .white,
insets: UIEdgeInsets(top: 5, left: 5, bottom: 10, right: 3))
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override func layoutSubviews() {
super.layoutSubviews()
}
func config(with viewModel: GraphViewModelType) {
self.viewModel = viewModel
yieldLabel.attributedText = viewModel.titleAttributeText
yieldPercentLabel.attributedText = viewModel.yielAttributeText
startDateLabel.attributedText = viewModel.startDateAttributeText
endDateLabel.attributedText = viewModel.endDateAttributeText
graphButtonView.confgiureCell(with: self.viewModel.btnData)
setUpChart()
}
func setUpChart() {
lineChart.delegate = self
lineChart.noDataText = "No Data Available"
lineChart.rightAxis.enabled = false
lineChart.leftAxis.enabled = false
lineChart.xAxis.enabled = false
lineChart.legend.enabled = false
lineChart.xAxis.drawGridLinesEnabled = false
lineChart.drawMarkers = true
lineChart.doubleTapToZoomEnabled = false
lineChart.pinchZoomEnabled = false
lineChart.scaleXEnabled = false
lineChart.scaleYEnabled = false
marker.chartView = lineChart
marker.minimumSize = CGSize(width: 28, height: 20)
lineChart.marker = marker
let dataSets = viewModel.getLineChartDataSet()
let data = LineChartData(dataSets: dataSets)
data.setValueFont(.systemFont(ofSize: 7, weight: .light))
lineChart.data = data
}
extension GraphTableViewCell: ChartViewDelegate {
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
let transform = lineChart.getTransformer(forAxis: .left)
let point = transform.pixelForValues(x: highlight.x, y: highlight.y)
print("point : x = \(point.x) y = \(point.y)")
let color = self.lineChart.colorOfPoint(point: point)
marker.color = color
let dataSets = viewModel.chartValueSelected(entry: entry)
lineChart.data = dataSets
}
}
This is the viewModel:
final class GraphTableViewCellViewModel: GraphViewModelType {
var startDateAttributeText = NSMutableAttributedString()
var endDateAttributeText = NSMutableAttributedString()
var titleAttributeText = NSMutableAttributedString(string: "", attributes: [NSAttributedString.Key.font : UIFont(name: "Orion-Bold", size: 14)!, NSAttributedString.Key.foregroundColor : UIColor.black])
var yielAttributeText = NSMutableAttributedString(string: "\(String(format: "%.2f%%", abs(14)))", attributes: [NSAttributedString.Key.font : UIFont(name: "Orion-Bold", size: 14)!, NSAttributedString.Key.foregroundColor : UIColor.green])
let disposeBag = DisposeBag()
var btnData: [AssetGraphButtonViewViewModel]
var data: TwrGraph
var dataSet = [LineChartDataSet]()
var set: LineChartDataSet!
var set1: LineChartDataSet!
init(with data: TwrGraph) {
btnData = AssetGraphViewModel(security: nil).assetGraphData
self.data = data
startDateAttributeText = NSMutableAttributedString(string: data.startDate, attributes: [NSAttributedString.Key.font : UIFont(name: "Orion-Regular", size: 12)!, NSAttributedString.Key.foregroundColor : ColorName.warmGreyTwo])
endDateAttributeText = NSMutableAttributedString(string: data.endDate, attributes: [NSAttributedString.Key.font : UIFont(name: "Orion-Regular", size: 12)!, NSAttributedString.Key.foregroundColor : ColorName.warmGreyTwo])
}
func getChartDataPoints(dataPoints: [String], values: [Double]) -> [ChartDataEntry] {
var dataEntries: [ChartDataEntry] = []
for count in (0..<dataPoints.count) {
dataEntries.append(ChartDataEntry.init(x: Double(count), y: values[count]))
}
return dataEntries
}
func getLineChartDataSet() -> [LineChartDataSet] {
let dataWeeklyDate = self.data.weeklyGraph.map { $0.date }
let dataWeeklyYield = self.data.weeklyGraph.map { $0.yield }
let dataPoints = getChartDataPoints(dataPoints: dataWeeklyDate, values: dataWeeklyYield)
set = LineChartDataSet(entries: dataPoints, label:"")
setup(set)
return [set]
}
func setup(_ dataSet: LineChartDataSet) {
dataSet.drawHorizontalHighlightIndicatorEnabled = false
dataSet.drawVerticalHighlightIndicatorEnabled = true
dataSet.isDrawLineWithGradientEnabled = true
dataSet.fillAlpha = 0.15
dataSet.lineWidth = 2
dataSet.circleRadius = 0
dataSet.drawCircleHoleEnabled = false
dataSet.drawCirclesEnabled = false
dataSet.drawValuesEnabled = false
dataSet.highlightColor = .blue
let rightColor = [ChartColorTemplates.colorFromString("#FA3A7A"), ChartColorTemplates.colorFromString("#C257B1"),
ChartColorTemplates.colorFromString("#8B73E8")]
dataSet.colors = rightColor
dataSet.gradientPositions = [0, 40, 100]
let gradientColors = [ChartColorTemplates.colorFromString("#FC4684").cgColor,
ChartColorTemplates.colorFromString("#D8D8D8").cgColor]
let colorLocations:[CGFloat] = [1.0, 0.0]
if let gradient = CGGradient(colorsSpace: nil, colors: gradientColors as CFArray, locations: colorLocations) {
dataSet.fill = LinearGradientFill(gradient: gradient, angle: 90.0)
}
dataSet.drawFilledEnabled = true
}
func updateSetAfterTouch(_ dataSet: LineChartDataSet) {
dataSet.drawHorizontalHighlightIndicatorEnabled = false
dataSet.drawVerticalHighlightIndicatorEnabled = true
// dataSet.isDrawLineWithGradientEnabled = true
dataSet.fillAlpha = 0.15
dataSet.lineWidth = 2
dataSet.circleRadius = 0
dataSet.drawCircleHoleEnabled = false
dataSet.drawCirclesEnabled = false
dataSet.drawValuesEnabled = false
// dataSet.highlightColor = .blue
dataSet.colors = [.red]
}
func chartValueSelected(entry: ChartDataEntry) -> LineChartData {
return updateSet(with: entry)
}
func updateSet(with entry: ChartDataEntry) -> LineChartData {
var dataEntries: [ChartDataEntry] = []
var dataEntries1: [ChartDataEntry] = []
for count in (0..<self.data.weeklyGraph.count) {
if count < self.data.weeklyGraph.count && count < Int(entry.x) {
dataEntries.append(ChartDataEntry.init(x: Double(count), y: self.data.weeklyGraph[count].yield))
} else {
dataEntries1.append(ChartDataEntry.init(x: Double(count), y: self.data.weeklyGraph[count].yield))
}
}
set = LineChartDataSet(entries: dataEntries, label:"")
set1 = LineChartDataSet(entries: dataEntries1, label:"")
setup(set)
updateSetAfterTouch(set1)
let data = LineChartData(dataSets: [set1, set])
return data
}
}
As you can see when value is selected and I am using it to create two ChartDataEntry and the two LineChartDataSet that one continue the other (in the x axis ). This is the image when we first entering(looks fine):
This is the image when selecting:
After creating two LineChartDataSet add a different colour for each set.
func updateGraph(){
//create 2 ChartDataEntry arrays for both sets
var dataEntries : [ChartDataEntry] = []
var dataEntries1 : [ChartDataEntry] = []
//use ur condition here to devide the data in to two groups
for i in 0..<numbers.count {
if i < 3{
let value = ChartDataEntry(x: Double(i), y: numbers[i]) // here we set the X and Y status in a data chart entry
dataEntries.append(value)
}else{
let value = ChartDataEntry(x: Double(i), y: numbers[i]) // here we set the X and Y status in a data chart entry
dataEntries1.append(value)
}
}
//I add the last element of the first array to the begining of the first array to stop discontinue
dataEntries1.insert(dataEntries.last!, at: 0)
//create data set 1
let set1 = LineChartDataSet(entries: dataEntries, label: "Number")
set1.colors = [UIColor.blue]
//create data set 2
let set2 = LineChartDataSet(entries: dataEntries1, label: "Number")
set2.colors = [UIColor.red]
//This is the object that will be added to the chart
let data = LineChartData(dataSets: [set1, set2])
chtChart.data = data //finally - it adds the chart data to the chart and causes an update
}
My Output looks like follows.
Full ViewController Code
class ViewController: UIViewController {
#IBOutlet weak var txtTextBox: UITextField!
#IBOutlet weak var chtChart: LineChartView!
var numbers : [Double] = [] //This is where we are going to store all the numbers. This can be a set of numbers that come from a Realm database, Core data, External API's or where ever else
override func viewDidLoad() {
super.viewDidLoad()
var dataEntries : [ChartDataEntry] = []
numbers = [2,4,7,3,4,5,8,9,1,2,9]
for i in 0..<numbers.count{
let value = ChartDataEntry(x: Double(i), y: numbers[i]) // here we set the X and Y status in a data chart entry
dataEntries.append(value)
}
let dataSet = LineChartDataSet(entries: dataEntries, label: "Number")
dataSet.drawHorizontalHighlightIndicatorEnabled = false
dataSet.drawVerticalHighlightIndicatorEnabled = true
dataSet.drawHorizontalHighlightIndicatorEnabled = true
dataSet.fillAlpha = 0.15
dataSet.lineWidth = 2
dataSet.circleRadius = 0
dataSet.drawCircleHoleEnabled = false
dataSet.drawCirclesEnabled = false
dataSet.drawValuesEnabled = false
dataSet.highlightColor = .blue
dataSet.colors = [UIColor.green]
let data = LineChartData(dataSets: [dataSet])
chtChart.data = data
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func btnbutton(_ sender: Any) {
// let input = Double(txtTextBox.text!) //gets input from the textbox - expects input as double/int
// numbers.append(input!) //here we add the data to the array.
updateGraph()
txtTextBox.text = ""
}
func updateGraph(){
//create 2 ChartDataEntry arrays for both sets
var dataEntries : [ChartDataEntry] = []
var dataEntries1 : [ChartDataEntry] = []
//use ur condition here to devide the data in to two groups
for i in 0..<numbers.count {
if i < 3{
let value = ChartDataEntry(x: Double(i), y: numbers[i]) // here we set the X and Y status in a data chart entry
dataEntries.append(value)
}else{
let value = ChartDataEntry(x: Double(i), y: numbers[i]) // here we set the X and Y status in a data chart entry
dataEntries1.append(value)
}
}
//I add the last element of the first array to the begining of the first array to stop discontinue
dataEntries1.insert(dataEntries.last!, at: 0)
//create data set 1
let set1 = LineChartDataSet(entries: dataEntries, label: "Number")
set1.colors = [UIColor.blue]
set1.drawHorizontalHighlightIndicatorEnabled = false
set1.drawVerticalHighlightIndicatorEnabled = true
set1.drawHorizontalHighlightIndicatorEnabled = true
set1.fillAlpha = 0.15
set1.lineWidth = 2
set1.circleRadius = 0
set1.drawCircleHoleEnabled = false
set1.drawCirclesEnabled = false
set1.drawValuesEnabled = false
set1.highlightColor = .blue
//create data set 2
let set2 = LineChartDataSet(entries: dataEntries1, label: "Number")
set2.colors = [UIColor.red]
set2.drawHorizontalHighlightIndicatorEnabled = false
set2.drawVerticalHighlightIndicatorEnabled = true
set2.drawHorizontalHighlightIndicatorEnabled = true
set2.fillAlpha = 0.15
set2.lineWidth = 2
set2.circleRadius = 0
set2.drawCircleHoleEnabled = false
set2.drawCirclesEnabled = false
set2.drawValuesEnabled = false
set2.highlightColor = .blue
//This is the object that will be added to the chart
let data = LineChartData(dataSets: [set1, set2])
chtChart.data = data //finally - it adds the chart data to the chart and causes an update
}
}
I am using Charts library for rendering line chart but I am unable to reload the data as I am trying to fetch the data from the API, there is a method given notifyDataSetChanged() but not working. I am using the chart within tableView cell. If anybody has some idea please help me out..............................................
import UIKit
import Charts
class ChartViewCell: UITableViewCell, ChartViewDelegate {
#IBOutlet weak var lineChartViewContainer: UIStackView?
var yVlaues = [ChartDataEntry]()
lazy var lineChartView: LineChartView = {
let chartView = LineChartView()
return chartView
}()
override func awakeFromNib() {
super.awakeFromNib()
setLineChart()
loadData()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func setLineChart() {
lineChartView.delegate = self
lineChartView.noDataText = "No Data Found"
lineChartView.rightAxis.enabled = false
lineChartView.xAxis.drawGridLinesEnabled = false
lineChartView.leftAxis.drawGridLinesEnabled = false
lineChartView.legend.enabled = false
//lineChartView.leftAxis.labelFont = .boldSystemFont(ofSize: 14)
lineChartView.leftAxis.labelCount = 6
lineChartView.leftAxis.labelTextColor = .black
lineChartView.xAxis.labelPosition = .bottom
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
lineChartView.frame = CGRect(x: 10, y: 70, width: screenWidth - 20, height: 200)
setData()
self.addSubview(lineChartView)
}
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
print("Abhay x \(entry.x) y \(entry.y)")
}
func setData() {
let set1 = LineChartDataSet(entries: yVlaues)
set1.lineWidth = 1
set1.colors = [UIColor.colorFromHex(hexString: "#80005661")]
set1.drawVerticalHighlightIndicatorEnabled = true
set1.drawHorizontalHighlightIndicatorEnabled = false
set1.highlightColor = UIColor.colorFromHex(hexString: "#80005661")
set1.highlightLineWidth = 1.0
set1.drawValuesEnabled = false
set1.circleHoleColor = .white
set1.circleColors = [UIColor.colorFromHex(hexString: "#80005661")]
set1.circleRadius = 5
let data = LineChartData(dataSet: set1)
lineChartView.data = data
let startColor = UIColor.colorFromHex(hexString: "#80005661").cgColor
let endColor = UIColor.white.cgColor
let gradientColors = [startColor, endColor] as CFArray // Colors of the gradient
let colorLocations:[CGFloat] = [1.0, 0.0] // Positioning of the gradient
let gradient = CGGradient.init(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: gradientColors, locations: colorLocations) // Gradient Object
if let gradient = gradient {
set1.fill = Fill.fillWithLinearGradient(gradient, angle: 90.0) // Set the Gradient
}
set1.drawFilledEnabled = true // Draw the Gradient
}
func loadData() {
var params = [String: Any]()
guard let userId = Reusable.getUserInfo()?.id else { return }
params = ["user_id": userId]
WebServiceHelper.postWebServiceCall(Constants.baseURL + "performedWorkoutsGraph", params: params, isShowLoader: false, success: { (responceObj) in
let statusMsg = StatusBool(json: responceObj)
if statusMsg.status {
self.yVlaues.removeAll()
let responseJsonArr = responceObj["data"].arrayValue
for item in responseJsonArr {
let graphData = GraphDataModel(json: item)
let chartDataEntry = ChartDataEntry(x: Double(graphData.interval), y: Double(graphData.workouts))
self.yVlaues.append(chartDataEntry)
}
self.lineChartView.data?.notifyDataChanged()
self.lineChartView.notifyDataSetChanged()
} else {
CommonUtils.showToastMessage(message: statusMsg.message)
}
}
, failure: { (failure) in
print(failure)
})
}
}
I just found the solution but not the reloading tricks I m calling setData() inside my loadData() and it's working
I have used this library on swift called: iOS Charts https://github.com/danielgindi/ios-charts
I have two datasets and I want to set the position of data labels in one of the dataset to the bottom of the line, so the numbers can be visible and no overlapping happens. How can I do this?
I setup the chart as follows:
private func configureChart() {
lineChartView = LineChartView(frame: CGRect(x: 0, y: 60, width: self.view.frame.width, height: 200))
lineChartView?.delegate = self
lineChartView?.chartDescription?.enabled = false
lineChartView?.dragEnabled = true
lineChartView?.setScaleEnabled(false)
lineChartView?.pinchZoomEnabled = false
lineChartView?.rightAxis.enabled = false
lineChartView?.xAxis.valueFormatter = self
lineChartView?.xAxis.granularity = 1
lineChartView?.legend.form = .line
lineChartView?.animate(yAxisDuration: 0.3)
if let lineChartView = lineChartView {
dashboardHeaderView?.subviews.filter({ $0 is LineChartView }).forEach {
$0.removeFromSuperview()
}
dashboardHeaderView?.addSubview(lineChartView)
}
setupLineChartData()
}
func setupLineChartData() {
monthData = ReportModel.monthlyOveralInfo()
let costSet = self.provideLineData(type: .totalCost)
let incomeSet = self.provideLineData(type: .totalIncome)
let lineChartData = LineChartData(dataSets: [incomeSet, costSet])
lineChartView?.data = lineChartData
lineChartView?.setVisibleXRangeMaximum(5)
lineChartView?.moveViewToX(lineChartView?.chartXMax ?? 0)
}
private func provideLineData(type: SWMonthlyOverallType) -> LineChartDataSet {
var mainColor: UIColor = .black
var gradientFirstColor: UIColor = .clear
var gradientSecondColor: UIColor = .black
if type == .totalIncome {
mainColor = .myAppGreen
gradientFirstColor = .clear
gradientSecondColor = .myAppGreen
}
let totalCosts = monthData.compactMap({
$0.items.first(where: {$0.type == type})
})
var index: Double = -1
let values: [ChartDataEntry] = totalCosts.compactMap({
index += 1
return ChartDataEntry(x: index, y: $0.value)
})
let chartDataSet = LineChartDataSet(values: values, label: type.rawValue)
chartDataSet.resetColors()
chartDataSet.drawIconsEnabled = false
chartDataSet.setColor(mainColor)
chartDataSet.setCircleColor(mainColor)
chartDataSet.lineWidth = 1
chartDataSet.circleRadius = 3
chartDataSet.drawCircleHoleEnabled = true
chartDataSet.valueFont = .systemFont(ofSize: 9)
let gradientColors = [gradientFirstColor.cgColor,
gradientSecondColor.cgColor]
let gradient = CGGradient(colorsSpace: nil, colors: gradientColors as CFArray, locations: nil)
chartDataSet.fillAlpha = 0.5
if let gradient = gradient {
chartDataSet.fill = Fill(linearGradient: gradient, angle: 90)
}
chartDataSet.drawFilledEnabled = true
return chartDataSet
}
Just posting in case someone finds it to be useful:
chart.xAxis.labelPosition = .bottom
I'm using iOS-charts by danielGindi to display a line chart. The desired behavior is to show the left-axis, x-axis and horizontal gridlines, however, sometimes one or both of the axes is missing and/or one or more of the gridlines is missing.
My code is below.
class LineChartViewController: UIViewController {
let timeIntervalCount = 15
#IBOutlet weak var lineChartView: LineChartView!
override func viewDidLoad() {
super.viewDidLoad()
configureChart()
setChartValues()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func randomizeTapped(_ sender: UIButton) {
let count = Int(arc4random_uniform(20) + 3)
setChartValues(count)
}
#IBAction func configureTapped(_ sender: UIButton) {
configureChart()
lineChartView.setNeedsDisplay()
}
#IBAction func closeTapped(_ sender: UIButton) {
dismiss (animated: true, completion: nil)
}
fileprivate func configureChart () {
lineChartView.xAxis.labelPosition = .bottom
lineChartView.xAxis.drawGridLinesEnabled = false
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.xAxis.axisLineColor = UIColor.lightGray
//lineChartView.xAxis.setLabelCount(timeIntervalCount, force: true)
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.leftAxis.enabled = true
lineChartView.leftAxis.axisLineColor = UIColor.lightGray
lineChartView.leftAxis.drawAxisLineEnabled = true
lineChartView.leftAxis.drawGridLinesEnabled = true
lineChartView.leftAxis.gridColor = UIColor.lightGray
lineChartView.leftAxis.axisMinimum = 0
lineChartView.leftAxis.valueFormatter = self
lineChartView.rightAxis.enabled = false
lineChartView.legend.enabled = false
lineChartView.chartDescription?.enabled = false
}
fileprivate func setChartValues (_ count: Int = 15) {
let top = 23
let bottom = 8
let values = (bottom..<top).map { (i) -> ChartDataEntry in
let val = Double(arc4random_uniform(UInt32(count)) + 3)
return ChartDataEntry(x: Double(i), y: val)
}
let set1 = LineChartDataSet(values: values, label: "DataSet 1")
set1.colors = [UIColor.orange]
set1.drawValuesEnabled = false
set1.drawCirclesEnabled = false
let data = LineChartData(dataSet: set1)
self.lineChartView.data = data
}
}
extension LineChartViewController: IAxisValueFormatter {
func stringForValue(_ value: Double,
axis: AxisBase?) -> String {
return String(format: "%.0f", value) + "%"
}
}
I'm making a match-3 game using SpriteKit. Explanation: http://www.raywenderlich.com/75273/make-game-like-candy-crush-with-swift-tutorial-part-2. Please refer to swm93's comment on page 4
This is a tutorial but it seems to have a memory leak in the code. Could anyone possibly download this swift project file and find what causes the memory leak and give possible solutions?
The maker of this tutorial said that there is a memory leak in "handleSwipe(swap)" method and that we can fix it by adding "weak" to field declaration. I tried to write "weak var scene: GameScene?" but if I do, it says "scene is nil" even if I initialized it like this: "scene = GameScene(size: skView.bounds.size)" in "viewDidLoad()" function.
The rest of the classes can be downloaded through my link here.
Even the view controllers have been dismissed, the memory use percentage wouldn't decrease... If I call a GameViewController, dismiss it, then call it again, the memory use is twice. In other words,
PreViewController(18MB) segue-> GameViewController(75MB) dismiss-> PreViewController(75MB) segue-> GameViewController(104MB)
import UIKit
import SpriteKit
import AVFoundation
class GameViewController: UIViewController {
// The scene draws the tiles and cookie sprites, and handles swipes.
var scene: GameScene!
// The level contains the tiles, the cookies, and most of the gameplay logic.
// Needs to be ! because it's not set in init() but in viewDidLoad().
var level: Level!
var movesLeft = 0
var score = 0
#IBOutlet weak var targetLabel: UILabel!
#IBOutlet weak var movesLabel: UILabel!
#IBOutlet weak var scoreLabel: UILabel!
#IBOutlet weak var gameOverPanel: UIImageView!
#IBOutlet weak var shuffleButton: UIButton!
var tapGestureRecognizer: UITapGestureRecognizer!
#IBAction func dismiss(sender: UIButton) {
self.dismissViewControllerAnimated(true, completion: {})
}
lazy var backgroundMusic: AVAudioPlayer = {
let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3")
let player = AVAudioPlayer(contentsOfURL: url, error: nil)
player.numberOfLoops = -1
return player
}()
override func prefersStatusBarHidden() -> Bool {
return true
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
}
override func viewDidLoad() {
super.viewDidLoad()
// Configure the view.
let skView = view as! SKView
skView.multipleTouchEnabled = false
// Create and configure the scene.
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
// Load the level.
level = Level(filename: "Level_1")
scene.level = level
scene.addTiles()
scene.swipeHandler = handleSwipe
// Hide the game over panel from the screen.
gameOverPanel.hidden = true
shuffleButton.hidden = true
// Present the scene.
skView.presentScene(scene)
// Load and start background music.
backgroundMusic.play()
// Let's start the game!
beginGame()
}
func beginGame() {
movesLeft = level.maximumMoves
score = 0
updateLabels()
level.resetComboMultiplier()
scene.animateBeginGame() {
self.shuffleButton.hidden = false
}
shuffle()
}
func shuffle() {
// Delete the old cookie sprites, but not the tiles.
scene.removeAllCookieSprites()
// Fill up the level with new cookies, and create sprites for them.
let newCookies = level.shuffle()
scene.addSpritesForCookies(newCookies)
}
// This is the swipe handler. MyScene invokes this function whenever it
// detects that the player performs a swipe.
func handleSwipe(swap: Swap) {
// While cookies are being matched and new cookies fall down to fill up
// the holes, we don't want the player to tap on anything.
view.userInteractionEnabled = false
if level.isPossibleSwap(swap) {
level.performSwap(swap)
scene.animateSwap(swap, completion: handleMatches)
} else {
scene.animateInvalidSwap(swap) {
self.view.userInteractionEnabled = true
}
}
}
// This is the main loop that removes any matching cookies and fills up the
// holes with new cookies.
func handleMatches() {
// Detect if there are any matches left.
let chains = level.removeMatches()
// If there are no more matches, then the player gets to move again.
if chains.count == 0 {
beginNextTurn()
return
}
// First, remove any matches...
scene.animateMatchedCookies(chains) {
// Add the new scores to the total.
for chain in chains {
self.score += chain.score
}
self.updateLabels()
// ...then shift down any cookies that have a hole below them...
let columns = self.level.fillHoles()
self.scene.animateFallingCookies(columns) {
// ...and finally, add new cookies at the top.
let columns = self.level.topUpCookies()
self.scene.animateNewCookies(columns) {
// Keep repeating this cycle until there are no more matches.
self.handleMatches()
}
}
}
}
func beginNextTurn() {
level.resetComboMultiplier()
level.detectPossibleSwaps()
view.userInteractionEnabled = true
decrementMoves()
}
func updateLabels() {
targetLabel.text = String(format: "%ld", level.targetScore)
movesLabel.text = String(format: "%ld", movesLeft)
scoreLabel.text = String(format: "%ld", score)
}
func decrementMoves() {
--movesLeft
updateLabels()
if score >= level.targetScore {
gameOverPanel.image = UIImage(named: "LevelComplete")
showGameOver()
}
else if movesLeft == 0 {
gameOverPanel.image = UIImage(named: "GameOver")
showGameOver()
}
}
func showGameOver() {
gameOverPanel.hidden = false
scene.userInteractionEnabled = false
shuffleButton.hidden = true
scene.animateGameOver() {
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver")
self.view.addGestureRecognizer(self.tapGestureRecognizer)
}
}
func hideGameOver() {
view.removeGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer = nil
gameOverPanel.hidden = true
scene.userInteractionEnabled = true
beginGame()
}
#IBAction func shuffleButtonPressed(AnyObject) {
shuffle()
// Pressing the shuffle button costs a move.
decrementMoves()
}
}
,
import SpriteKit
class GameScene: SKScene {
// This is marked as ! because it will not initially have a value, but pretty
// soon after the GameScene is created it will be given a Level object, and
// from then on it will always have one (it will never be nil again).
var level: Level!
var swipeHandler: ((Swap) -> ())?
let TileWidth: CGFloat = 32.0
let TileHeight: CGFloat = 36.0
let gameLayer = SKNode()
let cookiesLayer = SKNode()
let tilesLayer = SKNode()
let cropLayer = SKCropNode()
let maskLayer = SKNode()
var swipeFromColumn: Int?
var swipeFromRow: Int?
var selectionSprite = SKSpriteNode()
// Pre-load the resources
let swapSound = SKAction.playSoundFileNamed("Chomp.wav", waitForCompletion: false)
let invalidSwapSound = SKAction.playSoundFileNamed("Error.wav", waitForCompletion: false)
let matchSound = SKAction.playSoundFileNamed("Ka-Ching.wav", waitForCompletion: false)
let fallingCookieSound = SKAction.playSoundFileNamed("Scrape.wav", waitForCompletion: false)
let addCookieSound = SKAction.playSoundFileNamed("Drip.wav", waitForCompletion: false)
// MARK: Game Setup
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder) is not used in this app")
}
override init(size: CGSize) {
super.init(size: size)
anchorPoint = CGPoint(x: 0.5, y: 0.5)
let background = SKSpriteNode(imageNamed: "Background")
addChild(background)
gameLayer.hidden = true
addChild(gameLayer)
let layerPosition = CGPoint(
x: -TileWidth * CGFloat(NumColumns) / 2,
y: -TileHeight * CGFloat(NumRows) / 2)
tilesLayer.position = layerPosition
gameLayer.addChild(tilesLayer)
// We use a crop layer to prevent cookies from being drawn across gaps
// in the level design.
gameLayer.addChild(cropLayer)
// The mask layer determines which part of the cookiesLayer is visible.
maskLayer.position = layerPosition
cropLayer.maskNode = maskLayer
// This layer holds the Cookie sprites. The positions of these sprites
// are relative to the cookiesLayer's bottom-left corner.
cookiesLayer.position = layerPosition
cropLayer.addChild(cookiesLayer)
// nil means that these properties have invalid values.
swipeFromColumn = nil
swipeFromRow = nil
// Pre-load the label font so prevent delays during game play.
SKLabelNode(fontNamed: "GillSans-BoldItalic")
}
func addSpritesForCookies(cookies: Set<Cookie>) {
for cookie in cookies {
// Create a new sprite for the cookie and add it to the cookiesLayer.
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.position = pointForColumn(cookie.column, row:cookie.row)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
// Give each cookie sprite a small, random delay.
sprite.alpha = 0
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(0.25, withRange: 0.5),
SKAction.group([
SKAction.fadeInWithDuration(0.25),
SKAction.scaleTo(1.0, duration: 0.25)
])
]))
}
}
func removeAllCookieSprites() {
cookiesLayer.removeAllChildren()
}
func addTiles() {
for row in 0..<NumRows {
for column in 0..<NumColumns {
// If there is a tile at this position, then create a new tile
// sprite and add it to the mask layer.
if let tile = level.tileAtColumn(column, row: row) {
let tileNode = SKSpriteNode(imageNamed: "MaskTile")
tileNode.position = pointForColumn(column, row: row)
maskLayer.addChild(tileNode)
}
}
}
// The tile pattern is drawn *in between* the level tiles. That's why
// there is an extra column and row of them.
for row in 0...NumRows {
for column in 0...NumColumns {
let topLeft = (column > 0) && (row < NumRows)
&& level.tileAtColumn(column - 1, row: row) != nil
let bottomLeft = (column > 0) && (row > 0)
&& level.tileAtColumn(column - 1, row: row - 1) != nil
let topRight = (column < NumColumns) && (row < NumRows)
&& level.tileAtColumn(column, row: row) != nil
let bottomRight = (column < NumColumns) && (row > 0)
&& level.tileAtColumn(column, row: row - 1) != nil
// The tiles are named from 0 to 15, according to the bitmask that is
// made by combining these four values.
let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3
// Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn.
if value != 0 && value != 6 && value != 9 {
let name = String(format: "Tile_%ld", value)
let tileNode = SKSpriteNode(imageNamed: name)
var point = pointForColumn(column, row: row)
point.x -= TileWidth/2
point.y -= TileHeight/2
tileNode.position = point
tilesLayer.addChild(tileNode)
}
}
}
}
// MARK: Conversion Routines
// Converts a column,row pair into a CGPoint that is relative to the cookieLayer.
func pointForColumn(column: Int, row: Int) -> CGPoint {
return CGPoint(
x: CGFloat(column)*TileWidth + TileWidth/2,
y: CGFloat(row)*TileHeight + TileHeight/2)
}
// Converts a point relative to the cookieLayer into column and row numbers.
func convertPoint(point: CGPoint) -> (success: Bool, column: Int, row: Int) {
// Is this a valid location within the cookies layer? If yes,
// calculate the corresponding row and column numbers.
if point.x >= 0 && point.x < CGFloat(NumColumns)*TileWidth &&
point.y >= 0 && point.y < CGFloat(NumRows)*TileHeight {
return (true, Int(point.x / TileWidth), Int(point.y / TileHeight))
} else {
return (false, 0, 0) // invalid location
}
}
// MARK: Detecting Swipes
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// Convert the touch location to a point relative to the cookiesLayer.
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
// If the touch is inside a square, then this might be the start of a
// swipe motion.
let (success, column, row) = convertPoint(location)
if success {
// The touch must be on a cookie, not on an empty tile.
if let cookie = level.cookieAtColumn(column, row: row) {
// Remember in which column and row the swipe started, so we can compare
// them later to find the direction of the swipe. This is also the first
// cookie that will be swapped.
swipeFromColumn = column
swipeFromRow = row
showSelectionIndicatorForCookie(cookie)
}
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
// If swipeFromColumn is nil then either the swipe began outside
// the valid area or the game has already swapped the cookies and we need
// to ignore the rest of the motion.
if swipeFromColumn == nil { return }
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
let (success, column, row) = convertPoint(location)
if success {
// Figure out in which direction the player swiped. Diagonal swipes
// are not allowed.
var horzDelta = 0, vertDelta = 0
if column < swipeFromColumn! { // swipe left
horzDelta = -1
} else if column > swipeFromColumn! { // swipe right
horzDelta = 1
} else if row < swipeFromRow! { // swipe down
vertDelta = -1
} else if row > swipeFromRow! { // swipe up
vertDelta = 1
}
// Only try swapping when the user swiped into a new square.
if horzDelta != 0 || vertDelta != 0 {
trySwapHorizontal(horzDelta, vertical: vertDelta)
hideSelectionIndicator()
// Ignore the rest of this swipe motion from now on.
swipeFromColumn = nil
}
}
}
// We get here after the user performs a swipe. This sets in motion a whole
// chain of events: 1) swap the cookies, 2) remove the matching lines, 3)
// drop new cookies into the screen, 4) check if they create new matches,
// and so on.
func trySwapHorizontal(horzDelta: Int, vertical vertDelta: Int) {
let toColumn = swipeFromColumn! + horzDelta
let toRow = swipeFromRow! + vertDelta
if toColumn < 0 || toColumn >= NumColumns { return }
if toRow < 0 || toRow >= NumRows { return }
// Can't swap if there is no cookie to swap with. This happens when the user
// swipes into a gap where there is no tile.
if let toCookie = level.cookieAtColumn(toColumn, row: toRow),
let fromCookie = level.cookieAtColumn(swipeFromColumn!, row: swipeFromRow!),
let handler = swipeHandler {
// Communicate this swap request back to the ViewController.
let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
handler(swap)
}
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
// Remove the selection indicator with a fade-out. We only need to do this
// when the player didn't actually swipe.
if selectionSprite.parent != nil && swipeFromColumn != nil {
hideSelectionIndicator()
}
// If the gesture ended, regardless of whether if was a valid swipe or not,
// reset the starting column and row numbers.
swipeFromColumn = nil
swipeFromRow = nil
}
override func touchesCancelled(touches: Set<NSObject>, withEvent event: UIEvent) {
touchesEnded(touches, withEvent: event)
}
// MARK: Animations
func animateSwap(swap: Swap, completion: () -> ()) {
let spriteA = swap.cookieA.sprite!
let spriteB = swap.cookieB.sprite!
// Put the cookie you started with on top.
spriteA.zPosition = 100
spriteB.zPosition = 90
let Duration: NSTimeInterval = 0.3
let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
moveA.timingMode = .EaseOut
spriteA.runAction(moveA, completion: completion)
let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
moveB.timingMode = .EaseOut
spriteB.runAction(moveB)
runAction(swapSound)
}
func animateInvalidSwap(swap: Swap, completion: () -> ()) {
let spriteA = swap.cookieA.sprite!
let spriteB = swap.cookieB.sprite!
spriteA.zPosition = 100
spriteB.zPosition = 90
let Duration: NSTimeInterval = 0.2
let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
moveA.timingMode = .EaseOut
let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
moveB.timingMode = .EaseOut
spriteA.runAction(SKAction.sequence([moveA, moveB]), completion: completion)
spriteB.runAction(SKAction.sequence([moveB, moveA]))
runAction(invalidSwapSound)
}
func animateMatchedCookies(chains: Set<Chain>, completion: () -> ()) {
for chain in chains {
animateScoreForChain(chain)
for cookie in chain.cookies {
// It may happen that the same Cookie object is part of two chains
// (L-shape or T-shape match). In that case, its sprite should only be
// removed once.
if let sprite = cookie.sprite {
if sprite.actionForKey("removing") == nil {
let scaleAction = SKAction.scaleTo(0.1, duration: 0.3)
scaleAction.timingMode = .EaseOut
sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),
withKey:"removing")
}
}
}
}
runAction(matchSound)
runAction(SKAction.waitForDuration(0.3), completion: completion)
}
func animateScoreForChain(chain: Chain) {
// Figure out what the midpoint of the chain is.
let firstSprite = chain.firstCookie().sprite!
let lastSprite = chain.lastCookie().sprite!
let centerPosition = CGPoint(
x: (firstSprite.position.x + lastSprite.position.x)/2,
y: (firstSprite.position.y + lastSprite.position.y)/2 - 8)
let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic")
scoreLabel.fontSize = 16
scoreLabel.text = String(format: "%ld", chain.score)
scoreLabel.position = centerPosition
scoreLabel.zPosition = 300
cookiesLayer.addChild(scoreLabel)
let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7)
moveAction.timingMode = .EaseOut
scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
}
func animateFallingCookies(columns: [[Cookie]], completion: () -> ()) {
var longestDuration: NSTimeInterval = 0
for array in columns {
for (idx, cookie) in enumerate(array) {
let newPosition = pointForColumn(cookie.column, row: cookie.row)
let delay = 0.05 + 0.15*NSTimeInterval(idx)
let sprite = cookie.sprite!
let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1)
longestDuration = max(longestDuration, duration + delay)
let moveAction = SKAction.moveTo(newPosition, duration: duration)
moveAction.timingMode = .EaseOut
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.group([moveAction, fallingCookieSound])]))
}
}
// Wait until all the cookies have fallen down before we continue.
runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}
func animateNewCookies(columns: [[Cookie]], completion: () -> ()) {
// wait that amount before we trigger the completion block.
var longestDuration: NSTimeInterval = 0
for array in columns {
let startRow = array[0].row + 1
for (idx, cookie) in enumerate(array) {
// Create a new sprite for the cookie.
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.position = pointForColumn(cookie.column, row: startRow)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
// fall after one another.
let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1)
// Calculate duration based on far the cookie has to fall.
let duration = NSTimeInterval(startRow - cookie.row) * 0.1
longestDuration = max(longestDuration, duration + delay)
let newPosition = pointForColumn(cookie.column, row: cookie.row)
let moveAction = SKAction.moveTo(newPosition, duration: duration)
moveAction.timingMode = .EaseOut
sprite.alpha = 0
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.group([
SKAction.fadeInWithDuration(0.05),
moveAction,
addCookieSound])
]))
}
}
// Wait until the animations are done before we continue.
runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}
func animateGameOver(completion: () -> ()) {
let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
action.timingMode = .EaseIn
gameLayer.runAction(action, completion: completion)
}
func animateBeginGame(completion: () -> ()) {
gameLayer.hidden = false
gameLayer.position = CGPoint(x: 0, y: size.height)
let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
action.timingMode = .EaseOut
gameLayer.runAction(action, completion: completion)
}
// MARK: Selection Indicator
func showSelectionIndicatorForCookie(cookie: Cookie) {
if selectionSprite.parent != nil {
selectionSprite.removeFromParent()
}
if let sprite = cookie.sprite {
let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
selectionSprite.size = texture.size()
selectionSprite.runAction(SKAction.setTexture(texture))
sprite.addChild(selectionSprite)
selectionSprite.alpha = 1.0
}
}
func hideSelectionIndicator() {
selectionSprite.runAction(SKAction.sequence([
SKAction.fadeOutWithDuration(0.3),
SKAction.removeFromParent()]))
}
}