title: Smooth scrolling with UIScrollView based on a timer, threads (based on iOS Metronome)
body: <p>I am working on an app that scrolls sheet music on the screen using a UIScrollView. As I need this to happen at regular time intervals and also play short sounds at evenly spaced intervales, I based my code on the now deprecated "Metronome" example provided by Apple.</p>
  
  <p>The problem is that the scrolling is not happening smoothly - its quite jerky. My run log indicates that the NSTimer I am using is not really firing at accurate intervals (or perhaps portions of code are taking too long to execute).</p>
  
  <p>Admission: I am a musician, not a professional programmer. I read the Apple documentation on GCD (which seems to be a better way to execute simultaneous events than the threading in the Metronome example), but I really couldn't figure out how to apply it to my project.</p>
  
  <p>I am not using paging. The content size is much larger than the screen size: the ScrollView scrolls, just jaggedly. </p>
  
  <p>My code executes fine, but the scrolling is very jerky. Any help would be appreciated, especially if its geared towards the KSS principle is observed!</p>
  
  <pre><code>//  PlayView.m
  #import "PlayView.h"
  #include &lt;stdlib.h&gt;
  
  NSInteger xWidth;
  int xChange = 0;
  float timeInterval;
  AVAudioPlayer *audioPlayer1;
  AVAudioPlayer *audioPlayer2;
  float tempo;
  int subdivisions;
  int timesPlayed = 1;
  int actualTimesPlayed = 0;
  
  // ...
  
  //RUN WHEN THE USER PRESSES THE PLAY BUTTON
  -(void)start {
    // Used in calculating the speed of timer firing.
    // xWidth is the spacing between images (pixels)
    subdivisions = (int)(xWidth);
  
    // Keeps track of where we are in the measure
    beatNumber = 0;
  
    // Keeps track of how many measures we already played
    timesPlayed = 1;
    actualTimesPlayed = 0;
  
    // Let the device idle without dimming the screen
    UIApplication *myApp=[UIApplication sharedApplication];
    myApp.idleTimerDisabled=YES;  
  
    [self startDriverThread];    
  }
  
  // Taken straight from 'Metronome'
  - (void)startDriverThread {
    if (soundPlayerThread != nil) {
      [soundPlayerThread cancel];
      [self waitForSoundDriverThreadToFinish];
    }
  
    NSThread *driverThread = [[NSThread alloc] initWithTarget:self  selector:@selector(startDriverTimer:) object:nil];
    self.soundPlayerThread = driverThread;
    [driverThread release];
  
    [self.soundPlayerThread start];
  }
  
  // Taken straight from 'Metronome'
  - (void)waitForSoundDriverThreadToFinish {
    while (soundPlayerThread &amp;&amp; ![soundPlayerThread isFinished]) { 
      // Wait for the thread to finish. I've experimented with different values
      [NSThread sleepForTimeInterval:timeInterval];
    }
  }
  
  // Taken straight from 'Metronome'
  - (void)stopDriverThread {
    [self.soundPlayerThread cancel];
    [self waitForSoundDriverThreadToFinish];
    self.soundPlayerThread = nil;
  }
  
  // Modification of 'Metronome'
  - (void)startDriverTimer:(id)info {
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  
      // Give the sound thread high priority to keep the timing steady.
      [NSThread setThreadPriority:1.0];
      BOOL continuePlaying = YES;
  
      while (continuePlaying) {  // Loop until cancelled.
  
        // An autorelease pool to prevent the build-up of temporary objects.
        NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init]; 
  
        // Reset the beat number to 0 at the end of each musical measure
        if (beatNumber == (subdivisions*4)) {
          beatNumber = 0; }
  
        // Incrementation of where we are in the bar on each firing of the timer
        beatNumber++;
  
        // On each beat, play a sound
        if(beatNumber % subdivisions == 0) {
          [self playSound]; }
  
        // On each firing of the timer, run the 'animateScreen' function, which scrolls the UIScrollView and performs some other simple tasks
        [self performSelectorOnMainThread:@selector(animateScreen) withObject:nil waitUntilDone:NO];
  
        // xChange is the number of pixels scrolled, but only start scrolling two and a half beats into the first bar (in order to keep the main image event at the center of the screen in each measure
        if ((actualTimesPlayed == 0 &amp;&amp; beatNumber &gt;= 2.5*subdivisions) || xChange &gt; 0) {
            xChange += 1; }    
  
        // The time interval at which the timer fires is calculated by dividing the tempo (beats per minute, entered by the user; between 60-94) and 60 (seconds) This alone would result in one firing of the timer per beat, but we need at double this speed for some of the calculations 'animateScreen' does in between the beats, and really many more so the scrolling is smooth.
        // 
        // EXAMPLE: (60s/92bpm)/17% (image spacing) of 320pixels (screen width) = 0.012077
        timeInterval = (60/tempo)/subdivisions; 
  
        NSDate *curtainTime = [[NSDate alloc] initWithTimeIntervalSinceNow:(timeInterval)];
        NSDate *currentTime = [[NSDate alloc] init];
  
      // Wake up periodically to see if we've been cancelled.
      while (continuePlaying &amp;&amp; ([currentTime compare:curtainTime] != NSOrderedDescending)) { 
          if ([soundPlayerThread isCancelled] == YES) {
              continuePlaying = NO;                
          }
  
          // Don't fully understand this; I've tried changing it to various values with no luck
          [NSThread sleepForTimeInterval:timeInterval];
          [currentTime release];
          currentTime = [[NSDate alloc] init];
      }
      [curtainTime release];      
      [currentTime release];      
      [loopPool drain];
  }
  [pool drain];
  
  }
  
  - (void)playSound {        
    if(beatNumber % subdivisions == 0){
      if (beatNumber == subdivisions) {
          [audioPlayer1 play];
      }
  
      else {
          [audioPlayer2 play];
      }
    }
  }
  
  - (void)animateScreen {        
    // BEAT 1
    if (beatNumber == (subdivisions)) {   
      // do some stuff
      // ...
    }    
  
    // THE SECOND  EIGTH OF 1
    if (beatNumber == (int)(subdivisions*1.25) &amp;&amp; actualTimesPlayed &gt;0) {
      // do some more stuff
      // ..
    }
  
    // BEAT 2
    if (beatNumber == (2*subdivisions)) {
      // even more stuff
    }
  
    // BEAT 3
    if (beatNumber == (3*subdivisions)) {
      // ... more
    }
  
    // BEAT 4
    if (beatNumber == (4*subdivisions)) {
      // yet more stuff
      // ...
  
      actualTimesPlayed++;
      timesPlayed++;
      if (timesPlayed == 3) {
          timesPlayed = 1; }     
    }    
  
    // On the "And of 4"
    if (beatNumber == subdivisions/2 &amp;&amp; actualTimesPlayed &gt; 0) {
      // STUFF
    }
  
    //Scroll over
    [theScroller setContentOffset:CGPointMake(xChange, 0) animated:NO];    
  }
  
  // ...
  </code></pre>
  
  <p>Perhaps this isn't the most elegant code, but with the exception of the scrolling, everything is functional. There is a lot going on where I've omitted code, but I know that stuff is not getting in the way (when I comment it out and leave the program totally bare bones - just the timer and scrolling, no sounds - it is still not smooth.) I'm convinced the timer is the problem.</p>
  
  <p>Any help/direction is very greatly appreciated.</p>