Re-initialise a UIView subclass in SWIFT - ios

I'm trying to make a iOS screen displaying a grid, the squares of which can be resized using a slider. currently i am just using a button to test incrementing the square size.
I have hacked some code from here:
Swift / UIView / drawrect - how to get drawrect to update when required
My problem is that, although drawrect IS being updated, I would like it to be initialised with each button press. The final response from the above link is beyond my meagre comprehension.
Here is my code:
import UIKit
let screenSize = UIScreen.mainScreen().bounds
var screenWidth = screenSize.width
var screenHeight = screenSize.height*0.9
var squareSize: Int = 10
class ViewController: UIViewController {
// create squares and initialise it with a frame
var squares = SquaresSubclass(frame: CGRectMake(0, 0, screenWidth, screenHeight))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(squares)
}
#IBAction func buttonPressed(sender: AnyObject) {
squareSize = squareSize + 1
squares.setNeedsDisplay()
}
}
class SquaresSubclass: UIView {
// initialise with the frame specified above
override init(frame: CGRect) {
super.init(frame: frame)
}
// no clue - copied from example
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
// loop CGRects across and down the screen
override func drawRect(rect: CGRect) {
var xPos = 5
let numberX = Int(0.98*screenWidth)/squareSize
var yPos = 20
let numberY = Int(0.96*screenHeight)/squareSize
for var y = 0; y<numberY ; y++ {
for var x = 0; x<numberX ; x++ {
let color:UIColor = UIColor.yellowColor()
let drect = CGRect(x: CGFloat(xPos), y: CGFloat(yPos), width: CGFloat(squareSize),height: CGFloat(squareSize))
let bpath:UIBezierPath = UIBezierPath(rect: drect)
color.set()
bpath.stroke()
xPos = xPos + squareSize
}
yPos = yPos + squareSize
xPos = 5
}
}
}

You're drawing more lines on top of the existing graphics. Clear the graphics context before you redraw the the grid:
CGContextClearRect(UIGraphicsGetCurrentContext(),self.bounds)

Any time you want the system to call drawRect: on a view for you, you call setNeedsDisplay() on that view.

Related

Swift callback function for draw()?

I have a customView where I overwrite drawRect:. I also have some cases, where I want to display a UILabel ontop of my customView:
if self.ticks.count < 50 {
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.addSubview(label)
}
However if I call this code before the drawRect is called, then my label doesn't show up. Is there a callback method like viewDidDraw or something?
EDIT
Someone asked for it, so here is all the code that is now doing what I need it to do. I think the answer to the question is to override layoutSubviews.
#IBDesignable open class CustomView: UIView {
#objc open var ticks:[Tick] = []
let label = UILabel()
required public init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
required public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup(){
self.backgroundColor = UIColor.clear
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.label.numberOfLines = 0
self.label.textAlignment = .center
self.addSubview(label)
}
override open func layoutSubviews() {
self.label.frame = self.bounds
}
#objc open func setSessions(sessions:[Session]){
self.ticks = []
for session in sessions.reversed(){
if self.ticks.count < 250{
self.ticks = self.ticks + (session.getShots() as! [Shot])
}else{
break
}
}
if self.ticks.count < 50 {
self.label.isHidden = false
}else{
self.label.isHidden = true
}
self.bringSubview(toFront: self.label)
}
override open func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let width = rect.width - 10;
let height = rect.height - 10;
let center = CGPoint(x: width/2+5, y: height/2+5)
let big_r = min(width, height)/2
context.setLineWidth(1.5)
UIColor.white.setStroke()
//draw the rings
for i in stride(from: 1, to: 7, by: 1){
let r = big_r * CGFloat(i) / 6.0;
context.addArc(center:center, radius:r, startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.strokePath()
}
//draw the ticks
if self.ticks.count > 49 {
UIColor.red().setFill()
for (i, tick) in self.ticks.prefix(250).enumerated(){
let radius = min((100.0-CGFloat(shot.score))/30.0 * big_r, big_r);
let x = Utilities.calculateX(tick, radius)
let y = Utilities.calculateY(tick, radius)
let point = CGPoint(x: x+center.x,y: y+center.y)
context.addArc(center:point, radius:CGFloat(DOT_WIDTH/2), startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.fillPath()
}
}
}
}
You can make your custom view redraw by calling setNeedsDisplay() when needed. After that, you can add the label
You can set a delegation for the View class and call the delegate method at the end of the draw function. However, it seems that if you call that directly, the draw function will only end until the delegate function get processed. Which means that the code will be executed before the new drawing refresh on the screen. In that case, you may need to call it asynchronously.
// at end of draw function
DispatchQueue.main.async {
self.delegate?.drawEnd()
}

iOS 10 custom navigation bar height

I implemented custom navigation bar height, by subclassing it with following code
class TMNavigationBar: UINavigationBar {
///The height you want your navigation bar to be of
static let navigationBarHeight: CGFloat = 44.0
///The difference between new height and default height
static let heightIncrease:CGFloat = navigationBarHeight - 44
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
let shift = TMNavigationBar.heightIncrease/2
///Transform all view to shift upward for [shift] point
self.transform =
CGAffineTransformMakeTranslation(0, -shift)
}
override func layoutSubviews() {
super.layoutSubviews()
let shift = TMNavigationBar.heightIncrease/2
///Move the background down for [shift] point
let classNamesToReposition: [String] = ["_UINavigationBarBackground"]
for view: UIView in self.subviews {
if classNamesToReposition.contains(NSStringFromClass(view.dynamicType)) {
let bounds: CGRect = self.bounds
var frame: CGRect = view.frame
frame.origin.y = bounds.origin.y + shift - 20.0
frame.size.height = bounds.size.height + 20.0
view.frame = frame
}
}
}
override func sizeThatFits(size: CGSize) -> CGSize {
let amendedSize:CGSize = super.sizeThatFits(size)
let newSize:CGSize = CGSizeMake(amendedSize.width, TMNavigationBar.navigationBarHeight);
return newSize;
}
}
Following problem occurs only on iOS 10: (black space between bar & view)
No idea what's happening there. But in storyboard it's generated this warning, and there's no way to fix it in IB (warning only appears when i change subclass of navigation bar in IB).
Works on iOS 10, Swift 3.0:
extension UINavigationBar {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
let screenRect = UIScreen.main.bounds
return CGSize(width: screenRect.size.width, height: 64)
}
}
I checked Interface debugger and this is what i see (so basically it's trying to change navigation bar height, bit it's stays same and it's showing just black space - which is window color):
With later investigation i noticed that it's not calling: "_UINavigationBarBackground"
Then i checked view.classForCoder from fast enumeration, and discovered that key is changed to "_UIBarBackground", so i updated layoutSubviews():
override func layoutSubviews() {
super.layoutSubviews()
let shift = TMNavigationBar.heightIncrease/2
///Move the background down for [shift] point
let classNamesToReposition = isIOS10 ? ["_UIBarBackground"] : ["_UINavigationBarBackground"]
for view: UIView in self.subviews {
if classNamesToReposition.contains(NSStringFromClass(view.classForCoder)) {
let bounds: CGRect = self.bounds
var frame: CGRect = view.frame
frame.origin.y = bounds.origin.y + shift - 20.0
frame.size.height = bounds.size.height + 20.0
view.frame = frame
}
}
}
Cheers.

Force custom view to redraw (or invalidate) using a timer

I've created a custom widget, which is animated. Now my problem is that I can't redraw the view, when the corresponding data gets updated.
Just don't hold anything against me in the code. This is my first piece of code in swift and I haven't worked with neither swift nor with Objective-C :-D
And also I've read the following questions, but they didn't help me:
How to force a view to render itself?
what-is-the-most-robust-way-to-force-a-uiview-to-redraw
p.s. : I can see the output of print(digit.phase) in the console.
p.s.s: I've also used performSelectorOnMainThread for calling the setNeedsDisplay function
The code:
import UIKit
struct Digit {
var targetDigit: Int
var currentDigit: Int
var phase: Float
}
#IBDesignable class RollerCounter: UIView {
var view: UIView!
var viewRect: CGRect!
var intNumber: Int
var digits = [Digit]()
let baseY = 20
var timer: NSTimer?
#IBInspectable var number: Int {
get {
return intNumber
}
set(number) {
intNumber = number
digits = []
var tempNumber:Int = intNumber
while tempNumber > 0 {
digits.append(Digit(targetDigit: tempNumber % 10, currentDigit: Int(rand()) % 10, phase: 0.0))
tempNumber /= 10
}
}
}
//init
override init(frame: CGRect) {
// set properties:
intNumber = 1111
super.init(frame: frame)
// setup the thing!
setup()
}
required init?(coder aDecoder: NSCoder) {
intNumber = 1111
super.init(coder: aDecoder)
// setup the thing
setup()
}
// Inital setup
func setup() {
let viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
view = UIView(frame: viewRect)
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
addSubview(view)
self.setNeedsDisplay()
backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.0)
}
func animate() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.016, target: self, selector: Selector("tick"), userInfo: nil, repeats: true)
}
func tick() {
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
setNeedsDisplay()
//TEST: Also tested this
// if let rect = viewRect {
// drawRect(rect)
// } else {
// viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
// drawRect(viewRect
// }
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
var tempNumber: Int = number
let strTempNumber = String(tempNumber)
var index: Int = 1
let width = Float(rect.width)
let charWidth: Float = Float(rect.width) / Float(strTempNumber.characters.count)
let charHeight: CGFloat = 36
let color = UIColor.blackColor()
let font: UIFont = UIFont(name: "Helvetica Neue", size: charHeight)!
let paraStyle = NSMutableParagraphStyle()
paraStyle.lineSpacing = 6.0
let skew = 0.1
let textAttribs = [
NSForegroundColorAttributeName: color,
NSParagraphStyleAttributeName: paraStyle,
NSObliquenessAttributeName: skew,
NSFontAttributeName: font
]
for digit in digits {
let strCurrentDigit: NSString = String(digit.currentDigit) as NSString
let strNextDigit: NSString = String(digit.currentDigit - 1) as NSString
let xPos = width - Float(index) * charWidth
let yPos = Float(baseY) + Float(charHeight) * digit.phase
let point: CGPoint = CGPoint(x: Int(xPos), y: Int(yPos))
strCurrentDigit.drawAtPoint(point, withAttributes: textAttribs)
let nextDigitYPos = yPos - Float(charHeight) * 1.2
let nextDigitPoint: CGPoint = CGPoint(x: Int(xPos), y: Int(nextDigitYPos))
strNextDigit.drawAtPoint(nextDigitPoint, withAttributes: textAttribs)
index++
tempNumber /= 10
}
}
}
Sorry folks. My bad :-(
There's nothing wrong with the invalidation system. Here's what's wrong:
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
As it turns out, the changes to phase only get reflected in the local digit instance inside the for loop
But just to be clear, the setNeedsDisplay() call inside the tick method is crucial for the view to be updated.

iOS custom view doesn't show image immediately

I made a simple custom view in Swift which has a background image view with a photo and some labels in front. I then added the custom view to my storyboard and it displays well, I can see the background image and labels.
But when I run my app on my device the image view doesn't show immediately, if I navigate to another view then back, it is displayed. I only have a timer which scheduled some tasks in background in my scene. Did I miss anything here?
Here is the code of my custom view
import UIKit
#IBDesignable class GaugeView: UIImageView {
#IBOutlet weak var speedLabel: UILabel!
let circlePathLayer = CAShapeLayer()
var circleRadius: CGFloat = 50.0
var speed: CGFloat {
get {
return circlePathLayer.strokeEnd
}
set {
speedLabel.text = String(format: "%.2f", newValue)
if newValue < 20.0 {
circlePathLayer.strokeColor = UIColor.blueColor().CGColor
} else if newValue >= 25.0 {
circlePathLayer.strokeColor = UIColor.redColor().CGColor
} else {
circlePathLayer.strokeColor = UIColor.yellowColor().CGColor
}
if (newValue > 50) {
circlePathLayer.strokeEnd = 1
} else if (newValue < 0) {
circlePathLayer.strokeEnd = 0
} else {
circlePathLayer.strokeEnd = newValue / 49.9
}
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
override func layoutSubviews() {
super.layoutSubviews()
circleRadius = bounds.width / 2 - 2.6
circlePathLayer.frame = bounds
circlePathLayer.path = circlePath().CGPath
}
// Our custom view from the XIB file
var view: UIView!
let nibName = "GaugeView"
func xibSetup() {
view = loadViewFromNib()
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(view)
// Configure speed circle layer
configure()
}
func configure() {
speed = 0.0
circlePathLayer.frame = bounds
circlePathLayer.lineWidth = 6
circlePathLayer.fillColor = UIColor.clearColor().CGColor
circlePathLayer.strokeColor = UIColor.blueColor().CGColor
layer.addSublayer(circlePathLayer)
backgroundColor = UIColor.whiteColor()
}
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
circleFrame.origin.x = CGRectGetMidX(circlePathLayer.bounds) - CGRectGetMidX(circleFrame)
circleFrame.origin.y = CGRectGetMidY(circlePathLayer.bounds) - CGRectGetMidY(circleFrame)
return circleFrame
}
func circlePath() -> UIBezierPath {
let center: CGPoint = CGPointMake(CGRectGetMidX(circlePathLayer.bounds), CGRectGetMidY(circlePathLayer.bounds))
let start = CGFloat(1.33 * M_PI_2)
let end = CGFloat( 1.64 * M_2_PI)
return UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
Then I added the custom view to my storyboard and navigate to the scene programatically with performSegueWithIdentifier("dashboardSegue", sender: nil). I just noticed not only the custome view, but two progress bar on the scene are not displayed as well.
It's better show your code, but i am guessing your timer stuck the main UI thread. When you navigate to another view, your timer.invalidate() was called. Try to remove the NSTimer to see if it works well.

Changing bounds of a child view centers it in its parent

I have been following a tutorial explaining a custom ios control. There is no problem with the tutorial but I got really confused about the frame/bounds clipping (not part of the tutorial).
I have a UIView instance in the scene in the storyboard. This UIView is sized at 120 x 120. I am adding a custom control (extending UIControl) to this container view with addSubview. I began to experiment with setting different widths and height to the frame and bounds of the custom control, this is the initializer of the control:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
}
...and produces this result (red is the parent, blue circle is the child):
Now I change the init to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.frame.size.width = 40
self.frame.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
And that produces this result:
and prints:
frame: (0.0,0.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
But when I change the initliazer to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
I get this:
and prints: frame: (40.0,40.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
I cannot seem to comprehend why this view is centered in its parent view when I change its bounds. What exactly is causing it? Is the 'frame' clipped always towards its center? Or is this view centered in its parent after the bounds have been modified and that causes the update of the 'frame'? Is it some property that can be changed? How could I manage to put it to top-left corner, for example (exactly in the same way as when I modify the 'frame')? Thanks a lot!
EDIT:
class ViewController: UIViewController {
#IBOutlet var knobPlaceholder: UIView!
#IBOutlet var valueLabel: UILabel!
#IBOutlet var valueSlider: UISlider!
#IBOutlet var animateSwitch: UISwitch!
var knob: Knob!
override func viewDidLoad() {
super.viewDidLoad()
knob = Knob(frame: knobPlaceholder.bounds)
knobPlaceholder.addSubview(knob)
knobPlaceholder.backgroundColor = UIColor.redColor();
}
#IBAction func sliderValueChanged(slider: UISlider) {
}
#IBAction func randomButtonTouched(button: UIButton) {
}
}
And the knob:
public class Knob: UIControl {
private let knobRenderer = KnobRenderer()
private var backingValue: Float = 0.0
/** Contains the receiver’s current value. */
public var value: Float {
get {
return backingValue
}
set {
setValue(newValue, animated: false)
}
}
/** Sets the receiver’s current value, allowing you to animate the change visually. */
public func setValue(value: Float, animated: Bool) {
if value != backingValue {
backingValue = min(maximumValue, max(minimumValue, value))
}
}
/** Contains the minimum value of the receiver. */
public var minimumValue: Float = 0.0
/** Contains the maximum value of the receiver. */
public var maximumValue: Float = 1.0
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createSublayers() {
knobRenderer.update(bounds)
knobRenderer.strokeColor = tintColor
knobRenderer.startAngle = -CGFloat(M_PI * 11.0 / 8.0);
knobRenderer.endAngle = CGFloat(M_PI * 3.0 / 8.0);
knobRenderer.pointerAngle = knobRenderer.startAngle;
knobRenderer.lineWidth = 2.0
knobRenderer.pointerLength = 6.0
layer.addSublayer(knobRenderer.trackLayer)
layer.addSublayer(knobRenderer.pointerLayer)
}
}
private class KnobRenderer {
let trackLayer = CAShapeLayer()
let pointerLayer = CAShapeLayer()
var strokeColor: UIColor {
get {
return UIColor(CGColor: trackLayer.strokeColor)
}
set(strokeColor) {
trackLayer.strokeColor = strokeColor.CGColor
pointerLayer.strokeColor = strokeColor.CGColor
}
}
var lineWidth: CGFloat = 1.0 {
didSet {
update();
}
}
var startAngle: CGFloat = 0.0 {
didSet {
update();
}
}
var endAngle: CGFloat = 0.0 {
didSet {
update()
}
}
var backingPointerAngle: CGFloat = 0.0
var pointerAngle: CGFloat {
get { return backingPointerAngle }
set { setPointerAngle(newValue, animated: false) }
}
func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
backingPointerAngle = pointerAngle
}
var pointerLength: CGFloat = 0.0 {
didSet {
update()
}
}
init() {
trackLayer.fillColor = UIColor.clearColor().CGColor
pointerLayer.fillColor = UIColor.clearColor().CGColor
}
func updateTrackLayerPath() {
let arcCenter = CGPoint(x: trackLayer.bounds.width / 2.0, y: trackLayer.bounds.height / 2.0)
let offset = max(pointerLength, trackLayer.lineWidth / 2.0)
let radius = min(trackLayer.bounds.height, trackLayer.bounds.width) / 2.0 - offset;
trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true).CGPath
}
func updatePointerLayerPath() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: pointerLayer.bounds.width - pointerLength - pointerLayer.lineWidth / 2.0, y: pointerLayer.bounds.height / 2.0))
path.addLineToPoint(CGPoint(x: pointerLayer.bounds.width, y: pointerLayer.bounds.height / 2.0))
pointerLayer.path = path.CGPath
}
func update(bounds: CGRect) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
func update() {
trackLayer.lineWidth = lineWidth
pointerLayer.lineWidth = lineWidth
updateTrackLayerPath()
updatePointerLayerPath()
}
}
In your update function you are centering the view.
func update(bounds: CGRect) {
**let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)**
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
If you take that out, then you're view won't be centered anymore. The reason setting the frame would leave it in the upper left hand corner while setting the bounds resulted in it being in the center is because setting the bounds x/y does not override the frame x/y. When you set the frame then later on your code only sets the bounds, so the frame x/y is never overwritten so the view stays in the upper left hand corner. However, in the second there is no x/y set for the frame, so I guess it's taken what you set for the bounds, so it get's centered.
I would recommend not setting the bounds x/y for the view as that should always be 0, 0. If you want to reposition it then use the frame. Remember the frame is relative the parent while the bounds is relative to it's self.

Resources