drawRect function not triggered when needDisplay is called - swift

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

Related

How to add UIGestureRecongnizer programmatically on custom views that are created via code?

I want to add a UITapGestureRecognizer to my view named SetView. My setviews are created programmatically on another custom view called GridView.
This is what I have tried so far but I am not seeing any action while tapping my subvies.
import UIKit
#IBDesignable
class GridView: UIView {
private(set) lazy var deckOfCards = createDeck()
lazy var grid = Grid(layout: Grid.Layout.fixedCellSize(CGSize(width: 128.0, height: 110.0)), frame: CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY), size: CGSize(width: bounds.width, height: bounds.height)))
lazy var listOfSetCard = createSetCards()
private func createDeck() -> [SetCard] {
var deck = [SetCard]()
for shape in SetCard.Shape.allShape {
for color in SetCard.Color.allColor {
for content in SetCard.Content.allContent {
for number in SetCard.Number.allNumbers {
deck.append(SetCard(shape: shape, color: color, content: content, rank: number))
}
}
}
}
deck.shuffle()
return deck
}
private func createSetCards() -> [SetView] {
var cards = [SetView]()
for _ in 0..<cardsOnScreen {
let card = SetView()
let contentsToBeDrawn = deckOfCards.removeFirst()
card.combinationOnCard.shape = contentsToBeDrawn.shape
card.combinationOnCard.color = contentsToBeDrawn.color
card.combinationOnCard.content = contentsToBeDrawn.content
card.combinationOnCard.rank = contentsToBeDrawn.rank
/* print(contentsToBeDrawn.color) */
addSubview(card)
cards.append(card)
}
return cards
}
override func layoutSubviews() {
super.layoutSubviews()
for index in listOfSetCard.indices {
let card = listOfSetCard[index]
if let rect = grid[index] {
card.frame = rect.insetBy(dx: 2.5, dy: 2.5)
card.frame.origin = rect.origin
print(card.frame.origin)
}
}
}
Here is the function didTap(sender: UITapGestureRecognizer) that I wrote on SetView:
#objc func didTap(sender: UITapGestureRecognizer) {
switch sender.state {
case .changed,.ended:
let rect = UIBezierPath(rect: bounds)
fillBoundingRect(inRect: rect, color: UIColor.gray)
default:
break
}
And ViewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...12 {
let card = game.drawModelCard()
game.deck.append(card)
}
}
lazy var game = SetGame()
weak var setView : SetView! {
didSet {
let tapGestureRecognizer = UITapGestureRecognizer(target:
setView, action: #selector(SetView.didTap(sender:)))
setView.isUserInteractionEnabled = true
setView.addGestureRecognizer(tapGestureRecognizer)
}
}
}
My subviews(SetViews) should change the background color once tapped.

I am trying to get an UIActivityIndicatorView to show when I am loading a UITableView

This problem has been answered several times before on this site, I have tried them all and none work. The difference I think is that I have a UITableView inside my UIViewController. I have tried when loading the data within viewDidLoad, here the screen I am coming from show until all is complete and my new view appears. I have also tried within viewDidAppear, here I have a blank table showing before the final view comes up.
I have tried 4 methods all from this site, I call pauseApp(n) before I start the load and restartApp(n) when completed
var spinner:UIActivityIndicatorView = UIActivityIndicatorView()
var loadingView = UIView()
var loadingLabel = UILabel()
var indicator = UIActivityIndicatorView()
#IBOutlet weak var tvTable: UITableView!
func pauseApp() {
tvTable.activityIndicatorView.startAnimating()
tvTable.activityIndicatorView.bringSubviewToFront(aIV)
UIApplication.shared.beginIgnoringInteractionEvents()
}
func pauseApp1() {
spinner = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
spinner.center = self.navBar.center
spinner.hidesWhenStopped = true
spinner.style = UIActivityIndicatorView.Style.gray
self.navigationController?.view.addSubview(spinner)
spinner.startAnimating()
UIApplication.shared.beginIgnoringInteractionEvents()
}
func pauseApp2() {
tvTable.activityIndicatorView.startAnimating()
indicator.startAnimating()
indicator.backgroundColor = UIColor.white
UIApplication.shared.beginIgnoringInteractionEvents()
}
func pauseApp3() {
setLoadingScreen()
UIApplication.shared.beginIgnoringInteractionEvents()
}
func restartApp() {
// sleep(2)
tvTable.activityIndicatorView.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
}
func restartApp1() {
spinner.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
}
func restartApp2() {
// sleep(2)
indicator.stopAnimating()
indicator.hidesWhenStopped = true
UIApplication.shared.endIgnoringInteractionEvents()
}
func restartApp3() {
// sleep(2)
removeLoadingScreen()
UIApplication.shared.endIgnoringInteractionEvents()
}
private func setLoadingScreen() {
let width: CGFloat = 120
let height: CGFloat = 30
let x = (view.frame.width / 2) - (width / 2)
let y = (view.frame.height / 2) - (height / 2) - 20
loadingView.frame = CGRect(x: x, y: y, width: width, height: height)
// Sets loading text
loadingLabel.textColor = .gray
loadingLabel.textAlignment = .center
loadingLabel.text = "Loading..."
loadingLabel.frame = CGRect(x: 0, y: 0, width: 140, height: 30)
// Sets spinner
spinner.style = .gray
spinner.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
spinner.startAnimating()
// Adds text and spinner to the view
loadingView.addSubview(spinner)
loadingView.addSubview(loadingLabel)
view.addSubview(loadingView)
view.bringSubviewToFront(loadingView)
}
private func removeLoadingScreen() {
spinner.stopAnimating()
spinner.isHidden = true
loadingLabel.isHidden = true
}
func activityIndicator() {
indicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
indicator.style = UIActivityIndicatorView.Style.gray
indicator.center = self.view.center
self.view.addSubview(indicator)
}
fileprivate var ActivityIndicatorViewAssociativeKey = "ActivityIndicatorViewAssociativeKey"
public var aIV: UIActivityIndicatorView = UIActivityIndicatorView()
public extension UITableView {
var activityIndicatorView: UIActivityIndicatorView {
get {
if let aIV = getAssociatedObject(&ActivityIndicatorViewAssociativeKey) as? UIActivityIndicatorView {
return aIV
} else {
let aIV = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
aIV.style = .gray
aIV.color = .gray
aIV.backgroundColor = UIColor.black
aIV.center = center
aIV.hidesWhenStopped = true
addSubview(aIV)
setAssociatedObject(aIV, associativeKey: &ActivityIndicatorViewAssociativeKey, policy: .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
aIV.bringSubviewToFront(aIV)
return aIV
}
}
set {
addSubview(newValue)
setAssociatedObject(newValue, associativeKey:&ActivityIndicatorViewAssociativeKey, policy: .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
public extension NSObject {
func setAssociatedObject(_ value: AnyObject?, associativeKey: UnsafeRawPointer, policy: objc_AssociationPolicy) {
if let valueAsAnyObject = value {
objc_setAssociatedObject(self, associativeKey, valueAsAnyObject, policy)
}
}
func getAssociatedObject(_ associativeKey: UnsafeRawPointer) -> Any? {
guard let valueAsType = objc_getAssociatedObject(self, associativeKey) else {
return nil
}
return valueAsType
}
}
Verify your Interface Builder file, specifically the order in which the components are defined. Components higher up in the hierarchy may be hidden by those defined below them. Thus it's quite possible that your tableview hides your activity view.
You should be able to confirm this fairly quickly by hiding the table view and other other views that may be on top. Depending on your activity view settings, you may also need to do tvTable.activityIndicatorView.isHidden = false. Note that since UITableView implement a built-in scrollview, adding an activity view as a child to a UITableView may not be the the best course. You are better off defining it as a child of the tableView's superview; ref:
Your attempt with pauseApp1 could work with minor modifications, but only if your view controller is hosted inside a navigation controller. You should also always define any relationship only AFTER the view is added as a subview not before.
Starting a brand new project from scratch, here's how you can display an activity indicator by code:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// We add some delay for fun, absolutely not required!
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.showSpinner()
}
}
private func showSpinner() {
let spinner = UIActivityIndicatorView.init(style: .gray)
self.view.addSubview(spinner)
spinner.center = self.view.center
spinner.startAnimating()
spinner.isHidden = false
spinner.hidesWhenStopped = true
}
}
Thanks Again,
The solution is to add an activityIndeicatorView with Storyboard below our TableView
Then in viewDidAppear have
#IBOutlet weak var mySpinner: UIActivityIndicatorView!
override func viewDidAppear(_ animated: Bool) {
self.pauseApp()
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.doScreen()
}
}
func pauseApp() {
showSpinner()
UIApplication.shared.beginIgnoringInteractionEvents()
}
func restartApp() {
mySpinner.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
}
private func showSpinner() {
mySpinner.startAnimating()
mySpinner.isHidden = false
mySpinner.hidesWhenStopped = true
}
The pauseApp call put the spinner on the screen, the doScreen does all the work and calls restartApp when it has finished. My doScreen does quite a lot of work going to a service to get the data, it takes about 2 seconds with a good internet connection, but much longer when the connection is poor.

Passing data between two *.swift files without prepareForSegue

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

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 make NSView draggable in Swift (OSX Mac app)?

I'm trying to make two NSView's draggable and I was wondering how to code that in Swift for OSX Mac app? I'm thinking to put two NSView's within an NSView and make them draggable within that NSView. For example, create NSViewA and nested within in are NSViewB and NSViewC (both draggable within NSViewA). I would like to code in Swift. Cheers!
You can do this fairly easily if you take advantage of the information passed on the responder chain. Consider this custom NSView class.
class DraggableView : NSView {
var startPoint: NSPoint?
var frameOrigin: NSPoint?
override func mouseDown(with event: NSEvent) {
startPoint = event.locationInWindow
frameOrigin = frame.origin
}
override func mouseDragged(with event: NSEvent) {
let offset = event.locationInWindow - startPoint!
frame.origin = frameOrigin! + offset
}
override func mouseUp(with event: NSEvent) {
startPoint = nil
frameOrigin = nil
}
}
To make the math work for NSPoint, I overloaded operators in the extension:
extension NSPoint {
static func -(pointA: NSPoint, pointB: NSPoint) -> NSPoint {
return NSPoint(x: pointA.x - pointB.x, y: pointA.y - pointB.y)
}
static func +(pointA: NSPoint, pointB: NSPoint) -> NSPoint {
return NSPoint(x: pointA.x + pointB.x, y: pointA.y + pointB.y)
}
}
Then your ViewController class is just setup code:
class ViewController: NSViewController {
let viewA: DraggableView = DraggableView()
let viewB: DraggableView = DraggableView()
override func viewDidLoad() {
super.viewDidLoad()
viewA.frame = NSRect(origin: .zero, size: NSSize(width: 100, height: 100))
viewB.frame = NSRect(origin: NSPoint(x: 125, y: 0), size: NSSize(width: 100, height: 100))
viewA.wantsLayer = true
viewB.wantsLayer = true
viewA.layer?.backgroundColor = NSColor.blue.cgColor
viewB.layer?.backgroundColor = NSColor.green.cgColor
view.addSubview(viewA)
view.addSubview(viewB)
}
}