-1

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.

enter image description here

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()
    }
}
hasan
  • 23,815
  • 10
  • 63
  • 101
  • Please refer: http://stackoverflow.com/questions/27212254/using-threads-to-update-ui-with-swift – Dheeraj D Jan 03 '17 at 09:20
  • First, ty for the info. 2nd, ya I am aware of that. we shouldn't update the ui from background thread. My problem is different. the ui itself is complicated. – hasan Jan 03 '17 at 09:24
  • I have got you... One thing i know is if application has no exception and not having bulk amount of data so it will never crash while tapping to different screen of the application. – Dheeraj D Jan 03 '17 at 09:26
  • It depend on the logic of the app. I can solve the crash by disabling the current view with dispatch. but, I wonna solve the freezing. on iphone 4 it takes minimum 5 sec. – hasan Jan 03 '17 at 09:28
  • The data that is to be presented on the UI is that causing the freezing ? – Omkar Guhilot Jan 03 '17 at 09:45
  • No, what causing the freezing is the Views. I have alot of views in there. no uiimageviews. mainly buttons with no images too. It freezes before I even do my http connection. Its not the data at all. its the views itself thats for sure. – hasan Jan 03 '17 at 09:47
  • The problem should be something other than there being simply too many `UIView`. Are you sure there isn't some infinite loop? You will have to show some code on what actions are fired just before the freezing happens so that we can help debug the issue. – Rikh Jan 03 '17 at 09:59
  • I am sure its the view. I will put a screen shot. – hasan Jan 03 '17 at 10:02
  • 1
    You should really use Instruments to figure out exactly where the app is stalling, and then see if you can improve the code that's responsible. – Tom Harrington Jan 03 '17 at 17:45

3 Answers3

3

Any time you need to touch anything involving the UI, put that code inside a dispatch to the main queue. For Swift 3, see the new DispatchQueue mechanism, otherwise, use the good ol' dispatch_async(...) and provide the main queue. A good tutorial on this is https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1.

BJ Miller
  • 1,536
  • 12
  • 16
  • I am not touching anything. the view is heavy and its freezes the screen until it gets loaded. I want to solve the freezing issue. any ideas? – hasan Jan 03 '17 at 17:27
  • Yes, any time you update anything involving UI, do it from the main queue. Any heavy processing, network requests, etc, should be done from a background queue. Once processing on those background queues completes, it is common to dispatch to the main queue to update the UI (such as your activity spinner). If you're doing any tasks on the main queue, put those into a background queue. – BJ Miller Jan 03 '17 at 17:30
  • You are not getting the situation yet bro. The UI is complicated and it is loaded in the main thread. but, beacuse its heavy it freezes the screen. I don't do any processing or http connection on the main thread. – hasan Jan 03 '17 at 17:32
  • The UI in the screenshot you sent doesn't look too "heavy", per se; I've created many with similar layouts and such without any issue. Perhaps you could post relevant code from your controller, otherwise I think anyone here is at a loss with how to help. A heavy UI may also be an indicator that your current view controller is doing too much, and you might consider breaking behaviors out into separate controllers/scenes. – BJ Miller Jan 03 '17 at 17:48
  • I did put in my question that the custom uibuttons(check boxes) is causing the problem. It look like there is no way to show an activity indicator that wouldnt freeze while loading the views. I will replace this custom uibuttons with a less complex one. – hasan Jan 03 '17 at 17:51
  • I understand you said that, but without any code, it's hard to help you. Please don't be patronizing, we are all here to help. – BJ Miller Jan 03 '17 at 17:54
  • I am cool bro. What I was asking about was obvious. I wanted to know if there is a way to load heavy UI without freezing the screen. Looks like there isn't a way. Thanky for trying to help. I really appreciate it. I know we all try to help on stack overflow :) – hasan Jan 03 '17 at 17:57
  • No problem. And to answer your question, yes there is a way. It is to put long-lasting or heavy-lifting operations into their own background thread(s), and have them inform the main queue (whose thread manages the UI) when they're done. But, without seeing any code, I don't know how to integrate it with your solution. As you probably know, all code is executed on the main thread unless specifically told to execute on a background thread. So, if you notice anything particularly heavy in your code, try putting it into the background. Nothing I see about your UI should cause any sluggishness. – BJ Miller Jan 03 '17 at 18:02
  • the problem was for the unnecessary calls for layoutIfNeeded() in all methods. to force calling drawrect setneedsdisplay is enough. ty for help. – hasan Jan 04 '17 at 13:10
  • 1
    Cool, glad you found the problem and fixed! +1 – BJ Miller Jan 04 '17 at 18:13
1

Apple Documentation says,

"If your application has a graphical user interface, it is recommended that you receive user-related events and initiate interface updates from your application’s main thread. This approach helps avoid synchronization issues associated with handling user events and drawing window content. Some frameworks, such as Cocoa, generally require this behavior, but even for those that do not, keeping this behavior on the main thread has the advantage of simplifying the logic for managing your user interface."

For more information, Click here

Imad Ali
  • 3,261
  • 1
  • 25
  • 33
  • My UI itself is complicated before I inject user data. shall I dig more in what you wrote? – hasan Jan 03 '17 at 09:27
  • If you're updating your UI with complicated views, the only option to show Activity Indicator (Block UI) & Update your UI on main thread only. – Imad Ali Jan 03 '17 at 09:29
  • ok, I can setuserinteraction. and I can show the activity indicator. but the freezing will not be solved. and the activity indicator itself will be freezed. right? – hasan Jan 03 '17 at 09:31
  • The activity indicator will show if I do it in a dispatch no problem. but, It will be freezed too. Thank you for help. any other ideas? – hasan Jan 03 '17 at 09:38
  • the problem was for the unnecessary calls for layoutIfNeeded() in all methods. I removed it and it worked perfectly. to force calling drawrect setneedsdisplay is enough. ty for help. the app was slow on all devices. – hasan Jan 04 '17 at 13:12
1

I have a child controller that have a complicated UI views.

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.

That causes the screen to freeze and it increases the chance of crashing if user kept tapping on the freezed screen.

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.

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.

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 your drawRect 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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1. I dont do any computing on where everything goes. all in storyboard connected with embeded segues. – hasan Jan 03 '17 at 18:30
  • 2. I figured out that the crash thing I pointed wasnt valid. that wasnt wht caused the crash that I reinvestigated. I will remove it from the question – hasan Jan 03 '17 at 18:31
  • Then why did you say "I know that the complixity is in the check boxes in the middle?" What did you mean by that? – Rob Napier Jan 03 '17 at 18:31
  • 3. I added the custom controls code. and draw rect code. I think the drawing is expensive nothing more. – hasan Jan 03 '17 at 18:33
  • Nothing there looks expensive to draw. Why do you think the drawing is expensive? (This is trivially testable; comment out drawRect and see if it's fast.) – Rob Napier Jan 03 '17 at 18:33
  • All other comments you did in your answer is very helpful. thank you. – hasan Jan 03 '17 at 18:34
  • I will comment the draw rect lines and let you know. – hasan Jan 03 '17 at 18:35
  • I did comment drawrect. You are right that didnt do it. and also when I removed all the code related to the checkboxes controller also didn't work. but, when I removed that child controller in the middle that only contains the checkboxes from the storyboard it opened very fast with no freeze. note: I am solving that slowleness on iphone 4. looks like the iphone 4 no longer can handle many views in same controller with iOS 9+ on it. I did mentioned that I am solving this on iphone 4 in one of the comments. but maybe you didnt know. I apologise if I wasted your time. thank you very much. – hasan Jan 03 '17 at 18:59
  • 1
    the problem was for the unnecessary calls for layoutIfNeeded() in all methods. I removed it and it worked perfectly. to force calling drawrect setneedsdisplay is enough. ty for help. the app was slow on all devices. – hasan Jan 04 '17 at 13:12
  • 1
    @hasan83 ah, you should almost never call layoutIfNeeded() directly unless you really need layout to be calculated before moving forward to the next line of code (which usually is because you're working in layout code already). You almost always mean setNeedsLayout(), and even that often isn't needed. – Rob Napier Jan 04 '17 at 13:59