Reputation: 673
I'm new to IOS and Swift, so I've started by porting Apple's Accelerometer example code to Swift.
This was all quite straightforward. Since the Accelerometer API has been deprecated, I used Core Motion instead, and it works just fine. I also switched to a storyboard.
The problem I have is that my layer delegate is only rarely called. It will go for a few minutes and never get called, and then it will get called 40 times a second, and then go back to not being called. If I context switch, the delegate will get called, and one of the sublayers will be displayed, but there are 32 sublayers, and I've yet to see them all get drawn. What's drawn seems to be fine - the problem is just getting the delegate to actually get called when I call setNeedsDisplay(), and getting all of the sublayers to get drawn.
I've checked to be sure that each sublayer has the correct bounds and frame dimensions, and I've checked to make sure that setNeedsDisplay() gets called after each accelerometer point is acquired.
If I attach an instrument, I see that the frame rate is usually zero, but occasionally it will be some higher number.
My guess is that the run loop isn't cycling. There's actually nothing in the run loop, and I'm not sure where to put one. In the ViewDidLoad delegate, I set up an update rate for the accelerometer, and call a function that updates the sublayers in the view. Everything else is event driven, so I don't know what I'd do with a run loop.
I've tried creating CALayers, and adding them as sublayers. I've also tried making the GraphViewSegment class a UIView, so it has it's own layer.
The version that's written in Objective C works perfectly reliably.
The way that this application works, is that acceleration values show up on the left side of the screen, and scroll to the right. To make it efficient, new acceleration values are written into a small sublayer that holds a graph for 32 time values. When it's full, that whole sublayer is just moved a pixel at a time to the right, and a new (or recycled) segment takes its place at the left side of the screen.
Here's the code that moves unchanged segments to the right by a pixel:
for s: GraphViewSegment in self.segments {
var position = s.layer.position
position.x += 1.0;
s.layer.position = position;
//s.layer.hidden = false
s.layer.setNeedsDisplay()
}
I don't think that the setNeedsDisplay is strictly necessary here, since it's called for the layer when the segment at the left gets a new line segment.
Here's how new layers are added:
public func addSegment() -> GraphViewSegment {
// Create a new segment and add it to the segments array.
var segment = GraphViewSegment(coder: self.coder)
// We add it at the front of the array because -recycleSegment expects the oldest segment
// to be at the end of the array. As long as we always insert the youngest segment at the front
// this will be true.
self.segments.insert(segment, atIndex: 0)
// this is now a weak reference
// Ensure that newly added segment layers are placed after the text view's layer so that the text view
// always renders above the segment layer.
self.layer.insertSublayer(segment.layer, below: self.text.layer)
// Position it properly (see the comment for kSegmentInitialPosition)
segment.layer.position = kSegmentInitialPosition;
//println("New segment added")
self.layer.setNeedsDisplay()
segment.layer.setNeedsDisplay()
return segment;
}
At this point I'm pretty confused. I've tried calling setNeedsDisplay all over the place, including the owning UIView. I've tried making the sublayers UIViews, and I've tried making them not be UIViews. No matter what I do, the behavior is always the same.
Everything is set up in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
pause.possibleTitles?.setByAddingObjectsFromArray([kLocalizedPause, kLocalizedResume])
isPaused = false
useAdaptive = false
self.changeFilter(LowpassFilter)
var accelerometerQueue = NSOperationQueue()
motionManager.accelerometerUpdateInterval = 1.0 / kUpdateFrequency
motionManager.startAccelerometerUpdatesToQueue(accelerometerQueue,
withHandler:
{(accelerometerData: CMAccelerometerData!, error: NSError!) -> Void in
self.accelerometer(accelerometerData)})
unfiltered.isAccessibilityElement = true
unfiltered.accessibilityLabel = "unfiltered graph"
filtered.isAccessibilityElement = true
filtered.accessibilityLabel = "filtered graph"
}
func accelerometer (accelerometerData: CMAccelerometerData!) {
if (!isPaused) {
let acceleration: CMAcceleration = accelerometerData.acceleration
filter.addAcceleration(acceleration)
unfiltered!.addPoint(acceleration.x, y: acceleration.y, z: acceleration.z)
filtered!.addPoint(filter.x, y: filter.y, z: filter.z)
//unfiltered.setNeedsDisplay()
}
}
Any idea?
I quite like Swift as a language - it takes the best parts of Java and C#, and adds some nice syntactic sugar. But this is driving me spare! I'm sure it's some little thing that I've overlooked, but I can't figure out what.
Upvotes: 2
Views: 579
Reputation: 93276
Since you've created a new NSOperationQueue
for your accelerometer updates handler, everything that handler calls is also running in a separate queue, sequestered from the main run loop. I'd suggest either running that handler on the main thread NSOperationQueue.mainQueue()
or moving anything that could update the UI back to the main thread via a block on the main queue:
NSOperationQueue.mainQueue().addOperationWithBlock {
// do UI stuff here
}
Upvotes: 1