我使用自定义NSView创建了一个NSProgressIndicator Spinner,如下所示,但是下面还会显示未提交的CATransaction错误。
我认为这是因为用于Spinner动画的后台线程。谁能告诉我:
a)使用后台线程的spinner代码是否有问题,如果有,那么替代方案是什么? b)如何防止CATransaction错误?
//
// Spinner.swift
// Sample
//
// Created by Duncan Groenewald on 18/12/2014.
// Copyright (c) 2014 Duncan Groenewald. All rights reserved.
//
import Cocoa
class Spinner: NSView {
// Some constants to control the animation
let kAlphaWhenStopped: CGFloat = 0.15
let kFadeMultiplier: CGFloat = 0.85
var position = 0
var numFins: Int = 12
var finColors = Array<NSColor>()
//NSColor *_finColors;
dynamic var animate: Bool = false {
didSet {
//FLog("called")
if animate {
startAnimation(self)
} else {
stopAnimation(self)
}
}
}
var isAnimating: Bool = false
var isFadingOut: Bool = false
var animationTimer: NSTimer? = nil
var animationThread: NSThread? = nil
var foreColor = NSColor.whiteColor()
var backColor = NSColor.yellowColor()
var displayedWhenStopped: Bool = true
// For determinate mode
var indeterminate: Bool = true
var currentValue: Double = 0.0
var color: NSColor? = nil
var backgroundColor = NSColor.clearColor()
var drawsBackground: Bool = true
var usesThreadedAnimation: Bool = true
var isIndeterminate: Bool = true
//var doubleValue: Double = 0.0
var maxValue: Double = 0.0
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
func commonInit() {
position = 0;
numFins = 12;
isAnimating = false
isFadingOut = false
loadColors()
backColor = NSColor.clearColor()
drawsBackground = false
displayedWhenStopped = true
usesThreadedAnimation = true
indeterminate = true
currentValue = 0.0;
maxValue = 100.0;
}
func loadColors() {
//FLog("called")
for var i=0; i<numFins; i++ {
finColors.append(foreColor)
}
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
//FLog(" called")
if self.hidden {
self.actuallyStopAnimation()
} else if isAnimating {
self.actuallyStartAnimation()
}
}
func startAnimation(sender: AnyObject?)
{
//FLog(" called")
if !indeterminate {
return
}
if isAnimating && !isFadingOut {
return
}
self.actuallyStartAnimation()
}
func stopAnimation(sender: AnyObject?)
{
//FLog(" called")
// animate to stopped state
isFadingOut = true
}
/// Only the spinning style is implemented
func setStyle(style: NSProgressIndicatorStyle)
{
if (NSProgressIndicatorStyle.SpinningStyle != style) {
assert(false, "Non-spinning styles not available.")
}
}
func setColorX(value: NSColor)
{
// FLog("called")
//FLog("set foreColor \(value)")
foreColor = value
// generate all the fin colors, with the alpha components
// they already have
for var i=0; i<numFins; i++ {
//FLog("set finColors")
let alpha: CGFloat = finColors[i].alphaComponent
finColors.append(foreColor.colorWithAlphaComponent(alpha))
}
self.needsDisplay = true
}
func setBackgroundColor(value: NSColor)
{
if backColor != value {
backColor = value;
self.needsDisplay = true
}
}
func setDrawsBackground(value: Bool)
{
if drawsBackground != value {
drawsBackground = value
}
self.needsDisplay = true
}
func setIndeterminate(isIndeterminate: Bool)
{
indeterminate = isIndeterminate;
if !indeterminate && isAnimating {
self.stopAnimation(self)
}
self.needsDisplay = true
}
func setDoubleValue(doubleValue: Double)
{
// Automatically put it into determinate mode if it's not already.
if indeterminate {
self.indeterminate = false
}
currentValue = doubleValue;
self.needsDisplay = true
}
func setMaxValue(maxValue: Double)
{
self.maxValue = maxValue;
self.needsDisplay = true
}
func setUsesThreadedAnimation(useThreaded: Bool)
{
if (self.usesThreadedAnimation != useThreaded) {
self.usesThreadedAnimation = useThreaded;
if (isAnimating) {
// restart the timer to use the new mode
self.stopAnimation(self)
self.startAnimation(self)
}
}
}
func setDisplayedWhenStopped(displayedWhenStopped: Bool)
{
self.displayedWhenStopped = displayedWhenStopped;
// Show/hide ourself if necessary
if (!isAnimating) {
if displayedWhenStopped && self.hidden {
self.hidden = false
}
else if !displayedWhenStopped && !self.hidden {
self.hidden = true
}
}
}
func updateFrame(sender: AnyObject?)
{
//FLog(" called \(position)")
if self.position > 0 {
self.position--
}
else {
self.position = self.numFins - 1
}
// update the colors
let minAlpha:CGFloat = self.displayedWhenStopped ? kAlphaWhenStopped : 0.01;
for var i=0; i<numFins; i++ {
// want each fin to fade exponentially over _numFins frames of animation
var newAlpha: CGFloat = self.finColors[i].alphaComponent * kFadeMultiplier
if newAlpha < minAlpha {
newAlpha = minAlpha
}
self.finColors[i] = self.foreColor.colorWithAlphaComponent(newAlpha)
//FLog(" finColor[\(i)] = ?")
}
if self.isFadingOut {
//FLog(" isFadingOut ")
// check if the fadeout is done
var done = true
for var i=0; i<self.numFins; i++ {
//FLog(" fabs = \(fabs(self.finColors[i].alphaComponent - minAlpha))")
if fabs(self.finColors[i].alphaComponent - minAlpha) > 0.01 {
done = false
break
}
}
if done {
self.actuallyStopAnimation()
}
}
else {
//FLog(" light up ")
// "light up" the next fin (with full alpha)
self.finColors[position] = self.foreColor
}
if self.usesThreadedAnimation {
// draw now instead of waiting for setNeedsDisplay (that's the whole reason
// we're animating from background thread)
self.display()
}
else {
self.needsDisplay = true
}
}
func actuallyStartAnimation()
{
//FLog(" called")
// Just to be safe kill any existing timer.
self.actuallyStopAnimation()
isAnimating = true
isFadingOut = false
// always start from the top
position = 1
if !self.displayedWhenStopped {
self.hidden = false
}
if self.window != nil {
FLog(" self.window != nil")
// Why animate if not visible? viewDidMoveToWindow will re-call this method when needed.
if self.usesThreadedAnimation {
animationThread = NSThread(target: self, selector: "animateInBackgroundThread", object: nil)
animationThread?.start()
}
else {
animationTimer = NSTimer(timeInterval: NSTimeInterval(0.05), target: self, selector: "updateFrame:", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.animationTimer!, forMode:NSRunLoopCommonModes)
NSRunLoop.currentRunLoop().addTimer(self.animationTimer!, forMode:NSDefaultRunLoopMode)
NSRunLoop.currentRunLoop().addTimer(self.animationTimer!, forMode:NSEventTrackingRunLoopMode)
}
} else {
//FLog(" self.window == nil")
}
}
func actuallyStopAnimation() {
//FLog(" called")
isAnimating = false
isFadingOut = false
if !self.displayedWhenStopped {
self.hidden = true
}
if self.animationThread != nil {
// we were using threaded animation
self.animationThread!.cancel()
if !self.animationThread!.finished {
NSRunLoop.currentRunLoop().runMode(NSModalPanelRunLoopMode, beforeDate:NSDate(timeIntervalSinceNow: 0.05))
}
self.animationThread = nil
}
else if self.animationTimer != nil {
// we were using timer-based animation
self.animationTimer!.invalidate()
self.animationTimer = nil
}
self.needsDisplay = true
}
func generateFinColorsStartAtPosition(startPosition: Int)
{
for var i=0; i<self.numFins; i++ {
let oldColor: NSColor = self.finColors[i]
let alpha = oldColor.alphaComponent
self.finColors[i] = self.foreColor.colorWithAlphaComponent(alpha)
}
}
func animateInBackgroundThread()
{
//FLog(" called")
// Set up the animation speed to subtly change with size > 32.
// int animationDelay = 38000 + (2000 * ([self bounds].size.height / 32));
// Set the rev per minute here
let omega: Int = 100 // RPM
let animationDelay = 60 * 1000000 / omega / numFins
var poolFlushCounter: Int = 0
do {
//FLog(" this is called")
updateFrame(nil)
usleep(useconds_t(animationDelay))
poolFlushCounter++;
if poolFlushCounter > 256 {
poolFlushCounter = 0
}
} while (!NSThread.currentThread().cancelled)
//FLog(" animateInBackgroundThread finished !")
}
override func drawRect(dirtyRect: NSRect) {
//FLog(" called")
let size: NSSize = self.bounds.size
//FLog(" size \(size)")
var theMaxSize: CGFloat = 0.0
// Set the size to the minimum dimension
if (size.width >= size.height) {
theMaxSize = size.height
} else {
theMaxSize = size.width
}
if self.drawsBackground {
backColor.set()
NSBezierPath(rect: self.bounds).fill()
}
if let currentContext = NSGraphicsContext.currentContext() {
var context = NSGraphicsContext.currentContext()!.CGContext
NSGraphicsContext.saveGraphicsState()
// Move the CTM so 0,0 is at the center of our bounds
let w2 = self.bounds.size.width / 2.0
let h2 = self.bounds.size.height / 2.0
CGContextTranslateCTM(context,w2,h2)
if (indeterminate) {
//FLog(" indeterminate")
var path = NSBezierPath()
let lineWidth: CGFloat = 0.0859375 * theMaxSize; // should be 2.75 for 32x32
let lineStart: CGFloat = 0.234375 * theMaxSize; // should be 7.5 for 32x32
let lineEnd: CGFloat = 0.421875 * theMaxSize; // should be 13.5 for 32x32
path.lineWidth = lineWidth
path.lineCapStyle = NSLineCapStyle.RoundLineCapStyle
path.moveToPoint(NSMakePoint(0, lineStart))
path.lineToPoint(NSMakePoint(0, lineEnd))
for var i = 0; i<numFins; i++ {
if isAnimating {
finColors[i].set()
} else {
foreColor.colorWithAlphaComponent(kAlphaWhenStopped).set()
}
path.stroke()
// we draw all the fins by rotating the CTM, then just redraw the same segment again
let r: CGFloat = 6.282185 / CGFloat(numFins)
CGContextRotateCTM(context, r)
}
} else {
//FLog(" !indeterminate")
let lineWidth:CGFloat = 1 + (0.01 * theMaxSize)
let circleRadius:CGFloat = (theMaxSize - lineWidth) / 2.1
let circleCenter:NSPoint = NSMakePoint(0, 0)
foreColor.set()
var path = NSBezierPath()
path.lineWidth = lineWidth
path.appendBezierPathWithOvalInRect(NSMakeRect(-circleRadius, -circleRadius, circleRadius*2, circleRadius*2))
path.stroke()
path = NSBezierPath()
let endAngle = 90.0 - (360.0 * currentValue / maxValue)
path.appendBezierPathWithArcWithCenter(circleCenter, radius: circleRadius, startAngle: 90.0, endAngle: CGFloat(endAngle), clockwise:true)
path.lineToPoint(circleCenter)
path.fill()
}
NSGraphicsContext.restoreGraphicsState()
}
}
}
CoreAnimation警告:
CoreAnimation: warning, deleted thread with uncommitted CATransaction; created by:
0 QuartzCore 0x00007fff845180ea _ZN2CA11Transaction4pushEv + 312
1 QuartzCore 0x00007fff84517f8a _ZN2CA11Transaction15ensure_implicitEv + 276
2 QuartzCore 0x00007fff8451d313 _ZN2CA5Layer13thread_flags_EPNS_11TransactionE + 37
3 QuartzCore 0x00007fff84526941 _ZN2CA5Layer13needs_displayEv + 45
4 QuartzCore 0x00007fff8452690c -[CALayer needsDisplay] + 21
5 AppKit 0x00007fff853576a3 -[NSView viewWillDraw] + 1078
6 AppKit 0x00007fff85356340 -[NSView _sendViewWillDrawInRect:clipRootView:] + 1417
7 AppKit 0x00007fff85337de6 -[NSView displayIfNeeded] + 1216
8 Sample 0x000000010004d6fd _TFC20SISU_Sample7Spinner11updateFramefS0_FGSqPSs9AnyObject__T_ + 2717
9 Sample 0x000000010004f78b _TFC20SISU_Sample7Spinner25animateInBackgroundThreadfS0_FT_T_ + 219
10 Sample 0x000000010004f8c2 _TToFC20SISU_Sample7Spinner25animateInBackgroundThreadfS0_FT_T_ + 34
11 Foundation 0x00007fff91b02b7a __NSThread__main__ + 1345
12 libsystem_pthread.dylib 0x00007fff87f5b2fc _pthread_body + 131
13 libsystem_pthread.dylib 0x00007fff87f5b279 _pthread_body + 0
14 libsystem_pthread.dylib 0x00007fff87f594b1 thread_start + 13
答案 0 :(得分:1)
看起来您正在后台线程中更改NSView的状态。 你不能这样做。
如果你调用lockFocusIfCanDraw,你可以能够绘制视图,虽然最后我总是放弃这些尝试......你不能安全地改变状态
if ( [view lockFocusIfCanDraw] )
{
[view drawRect:[view bounds]];
[view unlockFocus];
}
如果是我,我会放弃这种方法......你的主线程应该只用于UI交互&amp;图。
相反,任何花费超过0.2秒的任务都应该在后台线程中完成,但绘制不应该在后台线程中完成。 不幸的是,这说起来容易做起来难。