I have a child controller that have a complicated UI views. My View is designed in storyboard. That causes the screen to freeze and it increases the chance of crashing if user kept tapping on the freezed screen.
Is it possible that I load the UI in a different thread and show the user an activity indicator??
I know that the complixity is in the check boxes in the middle. the checkboxes is a custom uibuttons. I draw them on drawRect. depending on selection, border width, dynamic border color, dynamic selected border color, backgroundcolor, selection background color.
Edit: note that the superview tag is not 500. this is a multiselection view.
func setupCheckBox(checkbox: CheckBox) {
checkbox.setCornerRadius(radius: CGSize(width: checkbox.frame.size.width * 0.5, height: checkbox.frame.size.height * 0.5))
checkbox.setBorderWidth(width: 2.0)
checkbox.setBorderColor(border_color: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0))
checkbox.setSelection(selection_color: UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.9))
checkbox.setSelected(selection: false)
checkbox.setHit(edgeInsets: UIEdgeInsets(top: -4, left: -4, bottom: -4, right: -4))
checkbox.delegate = self
}
CheckBox implementation:
protocol CheckBoxProtocol: class {
func checkbox(checkBox: UIView, selection: Bool)
}
class CheckBox: RoundedCornersButton {
var checked_icon: String?
var unchecked_icon: String?
weak var delegate: CheckBoxProtocol?
var selectedd: Bool = false
var allow_none: Bool = false
var hitEdgeInsets: UIEdgeInsets?
var selection_color: UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setHit(edgeInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0))
selection_color = .clear
backgroundColor = .clear
addTarget(self, action: #selector(didTouchButton), for: .touchUpInside)
}
func setCheckedIcon(checked_icon: String) {
self.checked_icon = checked_icon
setSelectionTo(button: self, selected: selectedd, inform: nil)
}
func setUncheckedIcon(unchecked_icon: String) {
self.unchecked_icon = unchecked_icon
setSelectionTo(button: self, selected: selectedd, inform: nil)
}
func setSelection(selection_color: UIColor) {
self.selection_color = selection_color
setNeedsDisplay()
layoutIfNeeded()
}
// if superview tag is equal to 500. all other checkbox in the superview will deselected.
func didTouchButton() {
if superview?.tag == 500 {
for button: UIView in (superview?.subviews)! {
if button is CheckBox && button != self {
setSelectionTo(button: button as! CheckBox, selected: false, inform:delegate)
}
}
setSelectionTo(button: self as CheckBox, selected: allow_none ? (!selectedd) : true, inform:delegate)
} else {
setSelectionTo(button: self as CheckBox, selected: !selectedd, inform:delegate)
}
}
func setSelectionTo(button: CheckBox, selected: Bool, inform: CheckBoxProtocol?) {
button.selectedd = selected
if selected {
if checked_icon != nil {
(button as CheckBox).setImage(UIImage.init(named: checked_icon!), for: .normal)
}
if color != .clear {
button.setTitleColor(color, for: .normal)
}
} else {
if unchecked_icon != nil {
(button as CheckBox).setImage(UIImage.init(named: unchecked_icon!), for: .normal)
}
if selection_color != .clear {
button.setTitleColor(selection_color, for: .normal)
}
}
button.setNeedsDisplay()
button.layoutIfNeeded()
inform?.checkbox(checkBox: button, selection: selected)
}
func setSelected(selection: Bool) {
super.isSelected = selection
setSelectionTo(button: self, selected: selection, inform: nil)
setNeedsDisplay()
layoutIfNeeded()
}
func setHit(edgeInsets: UIEdgeInsets)
{
hitEdgeInsets = edgeInsets
}
// handeling hits on checkbox. taking in count the hitEdgeInsets. if hitEdgeInsets have minus values hits around the checkbox will be considered as a valid hits.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if UIEdgeInsetsEqualToEdgeInsets(hitEdgeInsets!, .zero) || !isEnabled || isHidden {
return super.point(inside: point, with: event)
}
let relativeFrame: CGRect = self.bounds
let hitFrame: CGRect = UIEdgeInsetsInsetRect(relativeFrame, hitEdgeInsets!)
return hitFrame.contains(point)
}
func isSelectedd() -> Bool {
return selectedd
}
func setAllowNone(_ allow: Bool) {
allow_none = allow
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context: CGContext = UIGraphicsGetCurrentContext()!
context.setFillColor((selection_color?.cgColor)!)
context.setStrokeColor((selection_color?.cgColor)!)
let circlePath: UIBezierPath = UIBezierPath.init(arcCenter: CGPoint(x: frame.size.width * 0.5, y: frame.size.width * 0.5), radius: frame.size.width * 0.5 - borderWidth, startAngle: 0, endAngle: CGFloat(2.0 * Float(M_PI)), clockwise: true)
if isSelectedd() {
circlePath.fill()
circlePath.stroke()
}
}
}
Rounded Corners Implementation:
class RoundedCornersButton: UIButton {
var cornorRadius: CGSize!
var borderWidth: CGFloat!
var borderColor: UIColor!
var color: UIColor!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
cornorRadius = CGSize(width: 12, height: 12)
borderWidth = 0
borderColor = UIColor.clear
self.titleLabel?.numberOfLines = 1
self.titleLabel?.adjustsFontSizeToFitWidth = true
self.titleLabel?.lineBreakMode = .byClipping
self.titleLabel?.baselineAdjustment = .alignCenters
self.titleLabel?.textAlignment = .center
// clear the background color to draw it on the graphics layer
color = self.backgroundColor
self.backgroundColor = UIColor.clear
}
func setColor(_color: UIColor) {
color = _color
self.setNeedsDisplay()
self.layoutIfNeeded()
}
func setCornerRadius(radius: CGSize) {
cornorRadius = radius
self.setNeedsDisplay()
self.layoutIfNeeded()
}
func setBorderWidth(width: CGFloat) {
borderWidth = width
self.setNeedsDisplay()
self.layoutIfNeeded()
}
func setBorderColor(border_color: UIColor) {
borderColor = border_color
self.setNeedsDisplay()
self.layoutIfNeeded()
}
// redraw without increasing or decreasing hiting area.
override func draw(_ rect: CGRect) {
// Drawing code
super.draw(rect)
// drawing context
let context: CGContext = UIGraphicsGetCurrentContext()!
// set fill color
context.setFillColor(color.cgColor)
// theme color
let themeColor: UIColor = borderColor
// set stroke color
context.setStrokeColor(themeColor.cgColor.components!)
// corners to round
let corners: UIRectCorner = UIRectCorner.allCorners
// bezier path rect
let bezierRect: CGRect = CGRect(x: rect.origin.x+borderWidth*0.5, y: rect.origin.y+borderWidth*0.5, width: rect.size.width-borderWidth, height: rect.size.height-borderWidth)
// rounded path
let roundedPath: UIBezierPath = UIBezierPath.init(roundedRect: bezierRect, byRoundingCorners: corners, cornerRadii: cornorRadius)
// set stroke width
roundedPath.lineWidth = borderWidth
// fill coloring
roundedPath.fill()
// stroke coloring
roundedPath.stroke()
}
}
What you've shown here aren't that complicated. I assume you mean that computing where everything goes is complicated. If that's true, you need to compute some time other than during the draw cycle.
If you're crashing due to the user interacting with a frozen view, then your problem isn't what you think it is. If you're hanging the event loop, all user touches are going to be ignored (since they're also on the event loop). But you may get a crash if you take too long to return from the event loop (~2s as I remember, though it may be shorter these days). See above; you must move any complex calculations somewhere else.
Excellent. Chasing down where the problem occurs is most of the solution. Your
drawRect
should be quite trivial for these, though. They're just circles. If yourdrawRect
is doing anything other than checking pre-computed values, then you need to move all that computation somewhere else.drawRect
generally should not compute very much. It has to be very fast.If your problem is that there is a lot to compute and the computation itself takes a long time, then you need to move that to a background queue, display a placeholder view while you're computing it, and when it's done draw the real view.
If your problem is that there are a lot more subviews than we're seeing, you may have to reduce the number of subviews and do more custom drawing (but the screenshot you've provided doesn't look that complicated).
But at the end of the day you must update the UI on the main queue, and you must not block the main queue. So if you have anything complicated to do, you need to do it on another queue and update the UI when it's finished.
If your problem is the actual time required to haul things out of the Storyboard, you can try extracting this piece into its own Storyboard or NIB, or draw it programmatically (or just reduce the number of elements in the Storyboard), but I've never personally encountered a case where that was the fix.