Passing data between two *.swift files without prepareForSegue - swift

I got two *.swift files. One is the ViewController with the slider.
The second one is a 'drawer' class which draws a line.
The goal is two (1) get a variable from the slider value then (2) pass it to 'drawer' to draw a line and then (3) inform the slider that it can pass a new value to redraw a line.
Should I use a Delegate to pass data or maybe it is too complicated?
Unfortunately I was unable to find a Delegate methods without prepareForSeague.
The code is as follows.
ViewController:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBAction func slider(_ sender: UISlider) {
label.text = String (sender.value)
}
drawer.swift:
import UIKit
// var position = 300 - Here I want to get a variable from the ViewController
class drawer: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setLineWidth(2.0)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let components: [CGFloat] = [0.0, 0.0, 1.0, 1.0]
let color = CGColor(colorSpace: colorSpace, components: components)
context?.setStrokeColor(color!)
context?.move(to: CGPoint(x: 30, y: 30))
context?.addLine(to: CGPoint(x: position, y: 400))
context?.strokePath()
}
}

You can declare the variable at the global level.
var position = 0.0
class drawer: UIView {}
And replace the value here.
#IBAction func slider(_ sender: UISlider) {
label.text = String(sender.value)
position = sender.value
}
Suggestion: For Naming convention: name all your classes starting with an uppercase letter

Related

drawRect function not triggered when needDisplay is called

I am trying to create a custom class to build various graph views for the app eventually. The main class is the viewController class where I would like to present the graphs managed by the clGraph class. clGraph class will be used to process the data and create a graph view by vGraph class.
plotGraph function in the vGraphs is where I am struggling. It appears that the drawRect function is not called, however, setNeedsDisplay is called. Below is the test code that I have to understand and learn the basics. Requesting help to be able to draw a basic line on the targetView. Thank you.
class main : NSViewController {
#IBOutlet weak var targetView: NSView!
func plotGraph(){
let g = clGraphs()
g.initialize(graphCanvas: targetView, controller: self)
g.processData()
g.plotline()
}
}
class clGraphs {
var controller = NSViewController()
var graphCanvas = NSView()
var lineData = [Float]()
public func initialize (graphCanvas : NSView, controller : NSViewController) -> clGraphs{
self.graphCanvas = graphCanvas
self.controller = controller
return self
}
public func plotline(){
let lb = NSTextField(frame: NSRect(x: 10, y: 10, width: 100, height: 20))
lb.stringValue = "Test" //works fine on the view
graphCanvas.addSubview(lb)
print(graphCanvas.frame) //works fine
let g = vGraphs()
g.graphCanvas = graphCanvas
g.needsDisplay = true
}
public func processData(){
//Some logic to update lineData
}
}
class vGraphs: NSView{
var graphCanvas = NSView()
var lineData = [Float]()
override func draw(_ dirtyRect: NSRect) {
print("In the draw rect function")
}
override func setNeedsDisplay(_ invalidRect: NSRect) {
print("In the set needs display function")
plotGraph()
}
func plotGraph(){
graphCanvas.layer?.backgroundColor = NSColor.blue.cgColor //works fine
let aPath = NSBezierPath()
aPath.move(to: NSPoint(x: 50, y: 50))
aPath.line(to: NSPoint(x: 200, y: 200))
aPath.lineWidth = 10
aPath.close()
aPath.stroke()
// ERROR: CGContextRestoreGState: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
}
}

Programmatically Setting AutoresizingMask to NSView Object

I wonder why it always fails whenever I try to set the autoresizingMask to an NSView object programmatically? In the following lines of code, I have a tiny, blue NSVIew object (topLeftView) set at the top-left corner of its parent container.
class ViewController: NSViewController {
// MARK: - IBOutlet
#IBOutlet weak var panView: PanView!
override func viewDidLoad() {
super.viewDidLoad()
makeImageContainer(point: CGPoint.zero)
}
func makeViewContainer(point: CGPoint) {
let myView = NSView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 150.0, height: 150.0))
myView.wantsLayer = true
if let myLayer = myView.layer {
myLayer.backgroundColor = NSColor.orange.cgColor
}
panView.addSubview(myView) // panView is an IBOutlet-connected `NSView` object.
/* tiny, blue `NSView` object */
let topLeftView = NSView(frame: CGRect(origin: CGPoint(x: 0.0, y: 150.0 - 6.0), size: CGSize(width: 6.0, height: 6.0)))
topLeftView.wantsLayer = true
topLeftView.autoresizingMask = [.minXMargin, .maxYMargin]
if let layer = topLeftView.layer {
layer.backgroundColor = NSColor.blue.cgColor
}
myView.addSubview(topLeftView)
}
}
So it has minXMargin and maxYMargin autoresizingMasks.
Now, I want to click on a push button to resize this orange NSView container object to see if the tiny, blue NSView object will stick at the top-left corner.
#IBAction func testClicked(_ sender: NSButton) {
for i in 0..<panView.subviews.count {
let subView = panView.subviews[i]
if i == 4 {
subView.setFrameSize(CGSize(width: 200.0, height: 200.0))
}
}
}
And the tiny guy has moved by 50 points to the right and 50 points to the bottom. What am I doing wrong? Thanks.

PNCircle chart will not update when increment by button press

I am new in iOS and I want to implement a framework call PNChart to my project. I would like to increment the circle chart by one but the UI will not update. Please help.
import UIKit
import PNChart
class ViewController: UIViewController {
//global var
let number = NSNumber(value: 70)
//obj outlet
#IBOutlet weak var circleChart: PNCircleChart!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
let myCgrect = CGRect(
x: self.view.bounds.width/2 - 40,
y: self.view.bounds.height/2 - 40,
width: 100, height: 100)
let circleChart = PNCircleChart(
frame: myCgrect, total: Int(100) as NSNumber!,
current: number, clockwise: false,
shadow: false, shadowColor: UIColor.red)
circleChart?.backgroundColor = UIColor.clear
circleChart?.strokeColor = UIColor.blue
circleChart?.stroke()
self.view.addSubview(circleChart!)
}
#IBAction func increaseCircle(_ sender: Any) {
_ = NSNumber(value: number.intValue + 1)
print("btnpressed")
}
}// end uiviewcontroller
In your function increaseCircle you aren't doing anything with the information. You need to feed in this incremented number into your circleChart and redraw it. Since you're customizing Gindi's library you probably have your own functions for doing this.

How to set a border only for one of the edges of UIView

In swift, is there a way to only set a border for top side of a UIView ?
There are many ways, but drawing the border yourself might offer a little more control. I would recommend subclassing a UIView and use a CAShapeLayer to accomplish this.
Something to the effect of (using Swift 3):
import UIKit
class TopBorderedView: UIView {
//decalare a private topBorder
fileprivate weak var topBorder: CAShapeLayer?
//declare a border thickness to allow outside access to setting it
var topThickness: CGFloat = 1.0 {
didSet {
drawTopBorder()
}
}
//declare public color to allow outside access
var topColor: UIColor = UIColor.lightGray {
didSet {
drawTopBorder()
}
}
//implment the draw method
fileprivate func drawTopBorder() {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: bounds.width, y: 0)
removeIfNeeded(topBorder)
topBorder = addBorder(from: start, to: end, color: topColor, thickness: topThickness)
}
//implement a private border drawing method that could be used for border on other sides if desired, etc..
fileprivate func addBorder(from: CGPoint, to: CGPoint, color: UIColor, thickness: CGFloat) -> CAShapeLayer {
let border = CAShapeLayer()
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
border.path = path.cgPath
border.strokeColor = color.cgColor
border.lineWidth = thickness
border.fillColor = nil
layer.addSublayer(border)
return border
}
//used to remove the border and make room for a redraw to be autolayout friendly
fileprivate func removeIfNeeded(_ border: CAShapeLayer?) {
if let bdr = border {
bdr.removeFromSuperlayer()
}
}
//override layoutSubviews() (probably debatable) and call the drawTopBorder method to draw and redraw if needed
override func layoutSubviews() {
super.layoutSubviews()
drawTopBorder()
}
}
For maximum reusability in the context of storyboards - I would also take a look at using #IBDesignable and #IBInspectable for common UI patterns like this. For a decent intro, checkout NSHipster: IBDesignable and IBInspectable

Creating a pie chart in swift

I'm trying to create a pie chart in swift, and would like to create the code from scratch rather than use a 3rd party extension.
I like the idea of it being #IBDesignable, so I started with this:
import Foundation
import UIKit
#IBDesignable class PieChart: UIView {
var data: Dictionary<String,Int>?
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override fun drawRect(rect: CGRect) {
// draw the chart in here
}
}
What I'm not sure about, is how best to get the data into the chart. Should I have something like this:
#IBOutlet weak var pieChart: PieChart!
override func viewDidLoad() {
pieChart.data = pieData
pieChart.setNeedsDisplay()
}
Or is there a better way? Presumably, there is no way to include the data in the init function?
Thanks in advance!
You could create a convenience init that includes the data, but that would only be useful if you are creating the view from code. If your view is added in the Storyboard, you will want a way to set the data after the view has been created.
It is good to look at the standard UI elements (like UIButton) for design clues. You can change properties on a UIButton and it updates without you having to call myButton.setNeedsDisplay(), so you should design your pie chart to work in the same manner.
It is fine to have a property of your view that holds the data. The view should take responsibility for redrawing itself, so define didSet for your data property and call setNeedsDisplay() there.
var data: Dictionary<String,Int>? {
didSet {
// Data changed. Redraw the view.
self.setNeedsDisplay()
}
}
Then you can simply set the data, and the pie chart will redraw:
pieChart.data = pieData
You can extend this to other properties on your pie chart. For instance, you might want to change the background color. You'd define didSet for that property as well and call setNeedsDisplay.
Note that setNeedsDisplay just sets a flag and the view will be drawn later. Multiple calls to setNeedsDisplay won't cause your view to redraw multiple times, so you can do something like:
pieChart.data = pieData
pieChart.backgroundColor = .redColor()
pieChart.draw3D = true // draw the pie chart in 3D
and the pieChart would redraw just once.
No, you cannot set the data in the init method if you have added this to a scene in a storyboard (because init(coder:) will be called).
So, yes, you could just populate the data for the pie chart in viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
pieChart.dataPoints = ...
}
But, because this PieChart is IBDesignable, that means that you probably wanted to see a rendition of the pie chart in IB. So you can implement prepareForInterfaceBuilder in the PieChart class, supplying some sample data:
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
dataPoints = ...
}
That way you can now enjoy the designable view (e.g., see a preview; other inspectable properties can be manifested) in Interface Builder. The preview is our sample data, not the data that will be shown at runtime, but it may be enough to appreciate the overall design:
And, as vacawama said, you'd want to move the setNeedsDisplay into the didSet observer for the property.
public class PieChart: UIView {
public var dataPoints: [DataPoint]? { // use whatever type that makes sense for your app, though I'd suggest an array (which is ordered) rather than a dictionary (which isn't)
didSet { setNeedsDisplay() }
}
#IBInspectable public var lineWidth: CGFloat = 2 {
didSet { setNeedsDisplay() }
}
...
}
Just in case anybody looks at this question again, I wanted to post my finished code so that it can be useful to others. Here it is:
import Foundation
import UIKit
#IBDesignable class PieChart: UIView {
var dataPoints: Dictionary<String,Double> = ["Alpha":1,"Beta":2,"Charlie":3,"Delta":4,"Echo":2.5,"Foxtrot":1.4] {
didSet { setNeedsDisplay() }
}
#IBInspectable var lineWidth: CGFloat = 1.0 {
didSet { setNeedsDisplay()
}
}
#IBInspectable var lineColor: UIColor = uicolor_normal {
didSet { setNeedsDisplay() }
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
// set font for labels
let fieldColor: UIColor = UIColor.darkGrayColor()
let fieldFont = uifont_piechartkey
var fieldAttributes: NSDictionary = [
NSForegroundColorAttributeName: fieldColor,
NSFontAttributeName: fieldFont!
]
// get the graphics context and prepare an inset box for the pie
let ctx = UIGraphicsGetCurrentContext()
let margin: CGFloat = lineWidth
let box0 = CGRectInset(self.bounds, margin, margin)
let keyHeight = CGFloat( ceil( Double(dataPoints.count) / 3.0) * 24 ) + 16
let side : CGFloat = min(box0.width, box0.height-keyHeight)
let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side-keyHeight)/2,side,side)
let radius : CGFloat = min(box.width, box.height)/2.0
// converts percentages to radians for drawing the segment
func percent_to_rad(p: Double) -> CGFloat {
let rad = CGFloat(p * 0.02 * M_PI)
return rad
}
// draws a segment
func draw_arc(start: CGFloat, end: CGFloat, color: CGColor) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, box.midX, box.midY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,box.midX,box.midY,radius-lineWidth/2,start,end,0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
// draws a key item
func draw_key(keyName: String, keyValue: Double, color: CGColor, keyX: CGFloat, keyY: CGFloat) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, keyX, keyY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,keyX,keyY,8,0,CGFloat(2 * M_PI),0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
keyName.drawInRect(CGRectMake(keyX + 12,keyY-8,self.bounds.width/3,16),withAttributes: fieldAttributes as? [String : AnyObject])
}
let total = Double(dataPoints.values.reduce(0, combine: +)) // the total of all values
// convert dictionary to sorted touples
let dataPointsArray = dictionary_to_sorted_array(dataPoints)
// now sort the dictionary into an Array
var start = -CGFloat(M_PI_2) // start at 0 degrees, not 90
var end: CGFloat
var i = 0
// draw all segments
for dataPoint in dataPointsArray {
end = percent_to_rad(Double( (dataPoint.value)/total) * 100 )+start
draw_arc(start,end:end,color: uicolors_chart[i%uicolors_chart.count].CGColor)
start = end
i++
}
// the key
var keyX = self.bounds.minX + 8
var keyY = self.bounds.height - keyHeight + 32
i = 0
for dataPoint in dataPointsArray {
draw_key(dataPoint.key, keyValue: dataPoint.value, color: uicolors_chart[i%uicolors_chart.count].CGColor, keyX: keyX, keyY: keyY)
if((i+1)%3 == 0) {
keyX = self.bounds.minX + 8
keyY += 24
} else {
keyX += self.bounds.width / 3
}
i++
}
}
}
This will create a pie chart, that looks something like this:
[
The other bits of code you'll need are the colours array:
let uicolor_chart_1 = UIColor.init(red: 0.0/255, green:153.0/255, blue:255.0/255, alpha:1.0) //16b
let uicolor_chart_2 = UIColor.init(red: 0.0/255, green:200.0/255, blue:120.0/255, alpha:1.0)
let uicolor_chart_3 = UIColor.init(red: 140.0/255, green:220.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_4 = UIColor.init(red: 255.0/255, green:240.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_5 = UIColor.init(red: 255.0/255, green:180.0/255, blue:60.0/255, alpha:1.0)
let uicolor_chart_6 = UIColor.init(red: 235.0/255, green:60.0/255, blue:150.0/255, alpha:1.0)
let uicolors_chart : [UIColor] = [uicolor_chart_1,uicolor_chart_2,uicolor_chart_3,uicolor_chart_4,uicolor_chart_5,uicolor_chart_6]
And the code to convert the dictionary to an array:
func dictionary_to_sorted_array(dict:Dictionary<String,Double>) ->Array<(key:String,value:Double)> {
var tuples: Array<(key:String,value:Double)> = Array()
let sortedKeys = (dict as NSDictionary).keysSortedByValueUsingSelector("compare:")
for key in sortedKeys {
tuples.append((key:key as! String,value:dict[key as! String]!))
}
return tuples
}