在 Swift 中的不同线程上加载 UI
Loading UI on a different thread in Swift
我有一个具有复杂 UI 视图的子控制器。我的视图是在故事板中设计的。这会导致屏幕冻结,如果用户继续点击冻结的屏幕,则会增加崩溃的可能性。
我可以在不同的线程中加载 UI 并向用户显示 activity 指标吗??
我知道复杂性在中间的复选框中。复选框是自定义的 uibuttons。我在 drawRect 上绘制它们。取决于选择、边框宽度、动态边框颜色、动态选择的边框颜色、背景颜色、选择背景颜色。
编辑:
注意superview标签不是500,这是多选视图
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
}
复选框实现:
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()
}
}
}
圆角实现:
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()
}
}
Apple 文档说,
"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."
更多信息,Click here
任何时候你需要触及涉及 UI 的任何东西时,将该代码放入主队列的调度中。对于 Swift 3,请参阅新的 DispatchQueue 机制,否则,请使用 good ol' dispatch_async(...) 并提供主队列。关于此的一个很好的教程是 https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1。
I have a child controller that have a complicated UI views.
您在这里展示的内容并没有那么复杂。我假设你的意思是计算一切都非常复杂。如果是这样,您需要计算绘制周期以外的一些时间。
That causes the screen to freeze and it increases the chance of crashing if user kept tapping on the freezed screen.
如果您因用户与冻结视图交互而崩溃,那么您的问题不是您认为的那样。如果您挂起事件循环,所有用户触摸都将被忽略(因为它们也在事件循环中)。但是如果你从事件循环到 return 的时间太长(我记得大约 2 秒,尽管现在可能更短),你可能会崩溃。看上面;您必须将任何复杂的计算转移到其他地方。
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.
非常好。找出问题发生的地方是大部分的解决方案。不过,您的 drawRect
对于这些来说应该是微不足道的。它们只是圆圈。如果您的 drawRect
除了检查预先计算的值之外还在做任何事情,那么您需要将所有计算转移到其他地方。 drawRect
一般应该不会计算太多。一定要非常快。
如果你的问题是有很多东西要计算,而计算本身需要很长时间,那么你需要把它移到后台队列,在你计算它的时候显示一个占位符视图,当它画完实景
如果您的问题是子视图比我们看到的多很多,您可能需要减少子视图的数量并进行更多自定义绘图(但您提供的屏幕截图看起来并不复杂).
但最终你必须更新主队列上的UI,而你不能阻塞主队列。所以如果你有什么复杂的事情要做,你需要在另一个队列上做,并在完成时更新 UI。
如果您的问题是将内容从 Storyboard 中拖出所需的实际时间,您可以尝试将此片段提取到它自己的 Storyboard 或 NIB 中,或者以编程方式绘制它(或者只是减少 Storyboard 中的元素数量) ,但我个人从未遇到过这样的情况。
我有一个具有复杂 UI 视图的子控制器。我的视图是在故事板中设计的。这会导致屏幕冻结,如果用户继续点击冻结的屏幕,则会增加崩溃的可能性。
我可以在不同的线程中加载 UI 并向用户显示 activity 指标吗??
我知道复杂性在中间的复选框中。复选框是自定义的 uibuttons。我在 drawRect 上绘制它们。取决于选择、边框宽度、动态边框颜色、动态选择的边框颜色、背景颜色、选择背景颜色。
编辑: 注意superview标签不是500,这是多选视图
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
}
复选框实现:
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()
}
}
}
圆角实现:
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()
}
}
Apple 文档说,
"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."
更多信息,Click here
任何时候你需要触及涉及 UI 的任何东西时,将该代码放入主队列的调度中。对于 Swift 3,请参阅新的 DispatchQueue 机制,否则,请使用 good ol' dispatch_async(...) 并提供主队列。关于此的一个很好的教程是 https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1。
I have a child controller that have a complicated UI views.
您在这里展示的内容并没有那么复杂。我假设你的意思是计算一切都非常复杂。如果是这样,您需要计算绘制周期以外的一些时间。
That causes the screen to freeze and it increases the chance of crashing if user kept tapping on the freezed screen.
如果您因用户与冻结视图交互而崩溃,那么您的问题不是您认为的那样。如果您挂起事件循环,所有用户触摸都将被忽略(因为它们也在事件循环中)。但是如果你从事件循环到 return 的时间太长(我记得大约 2 秒,尽管现在可能更短),你可能会崩溃。看上面;您必须将任何复杂的计算转移到其他地方。
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.
非常好。找出问题发生的地方是大部分的解决方案。不过,您的 drawRect
对于这些来说应该是微不足道的。它们只是圆圈。如果您的 drawRect
除了检查预先计算的值之外还在做任何事情,那么您需要将所有计算转移到其他地方。 drawRect
一般应该不会计算太多。一定要非常快。
如果你的问题是有很多东西要计算,而计算本身需要很长时间,那么你需要把它移到后台队列,在你计算它的时候显示一个占位符视图,当它画完实景
如果您的问题是子视图比我们看到的多很多,您可能需要减少子视图的数量并进行更多自定义绘图(但您提供的屏幕截图看起来并不复杂).
但最终你必须更新主队列上的UI,而你不能阻塞主队列。所以如果你有什么复杂的事情要做,你需要在另一个队列上做,并在完成时更新 UI。
如果您的问题是将内容从 Storyboard 中拖出所需的实际时间,您可以尝试将此片段提取到它自己的 Storyboard 或 NIB 中,或者以编程方式绘制它(或者只是减少 Storyboard 中的元素数量) ,但我个人从未遇到过这样的情况。