0

I've written a program that plays a queue of songs that is processed in a background task using delegates. I send responses back to AppController to move a NSSlider show progress of song being played and deleted the NSCollection View entity (the song) that just played. Works great most of the time, but after 2 or three songs; the entity disappears but the remaining objects do not rearrange (move up on the screen) until I use the mouse or keyboard to bring the main screen into focus. I've used several suggestions to other similar questions and generally get the same response. The code I'm using now:

-(void) songPlayer:(SongPlayer *)player removeEntity:(DesktopEntity *)entity {
    int i = 0;
    NSIndexSet *indexes;
    indexes = [NSIndexSet indexSetWithIndex:i];
    NSLog(@"about to try");
    @try {

    //    [_arrayController removeObjectAtArrangedObjectIndex:0];
        [_seqFile removeObjectAtIndex:0];
        [_songs removeObjectAtIndex:0];

        [_myCollectionView setContent:_songs];
        [_arrayController rearrangeObjects];
        [_myCollectionView setNeedsDisplay:YES];
    //    [_arrayController rearrangeObjects];
    //    [_window makeKeyAndOrderFront:self];
    //    [_myCollectionView setNeedsDisplay:YES];
    //    [self.seqFile reloadData];

    }
    @catch (NSException *exception) {
        NSLog(@"Ugly Ending Caught");

    }
    @finally {
        NSLog(@"Each Time");
        }
  }

I've left comment code in to show other things I've tried to the same result. I believe it has to to with refreshing NSView, but not sure how.

my threads are set up as follows:

songPlayer.h

@protocol SongPlayerDelegate;
@interface SongPlayer : NSObject <SliderDelegate> {
    id<SongPlayerDelegate> __unsafe_unretained delegate;
}
@property (nonatomic, assign) id<SongPlayerDelegate> delegate;
…
@end
@protocol SongPlayerDelegate <NSObject>
- (void)songPlayer:(SongPlayer *)player removeEntity:(id)enity;
@end

songPlayer.M

#import "SongPlayer.h"
@implementation SongPlayer
@synthesize delegate;
-(id) init {
    if (self = [super init]) {
    }
    ...
    return self;
}

-(id) playMyQueue:(NSMutableArray *)q {
    //playsong stuff
    …
    [self.delegate songPlayer:self removeEntity:entity];
}

appController.h

#import "SongPlayer.h"
@interface AppController : NSObject <SongPlayerDelegate>
@property (weak) IBOutlet NSCollectionView *myCollectionView;
@property IBOutlet NSArrayController *arrayController;
@property (strong) NSMutableArray *songs;

@property SongPlayer *myPlayer;

- (void)songPlayer:(SongPlayer *)player removeEntity:(id)enity;

appController.m

#import "AppController.h"
@implementation AppController
@synthesize songs = _songs;
-(void) awakeFromNib {
    _songs = [[NSMutableArray alloc] init];
    [_arrayController addObject:s];  
    …
    _myPlayer = [[SongPlayer alloc] init];
…
}
-(IBAction)previewSong:(id)sender {
    if ([_myPlayer qActive]) {
        [(SongPlayer *)_myPlayer stopQueue];
        [_songLabel setStringValue:@"playback has been stopped"];
    } else {
        if (![_myPlayer isDone]) {
            [(SongPlayer *)_myPlayer stopSong];
            [_songLabel setStringValue:@"playback has been stopped"];
    } else {
            [_queue removeAllObjects];
            _myPlayer.delegate = self;
            NSMutableArray *activeQueue =
                [[NSMutableArray alloc] initWithArray:_seqFile];
            [_myPlayer performSelectorInBackground:@selector(playMyQueue:)
                withObject:activeQueue];
            }
      }
}

-(void) songPlayer:(SongPlayer *)player removeEntity:(DesktopEntity *)entity {
    int i = 0;
    NSIndexSet *indexes;
    indexes = [NSIndexSet indexSetWithIndex:i];
    NSLog(@"about to try");
    @try {
        [_arrayController removeObjectAtArrangedObjectIndex:0];
        [_seqFile removeObjectAtIndex:0];
    }
    @catch (NSException *exception) {
        NSLog(@"Ugly Ending Caught");
    }
    @finally {
        NSLog(@"Each Time");
    }
}
Bob Fields
  • 25
  • 8
  • You must not attempt to update the GUI – and that includes modifying any model or controller properties bound to the GUI – from a background thread. All such modifications must be done on the main thread. – Ken Thomases Aug 09 '14 at 16:13
  • Thanks for your response. That is why I'm using the delegates. The removeEntity method is in the AppController of the main thread. It is executed when notified by the background program that the song has completed. – Bob Fields Aug 09 '14 at 16:23
  • Delegates don't solve any threading issues. An "AppController" is not "of" the main thread. Objects don't live on specific threads. If you have code which runs on a background thread and it's calling a method without using a threading-specific API such as `-performSelectorOnMainThread:...` or GCD with `dispatch_get_main_queue()` to specifically do it on the main thread, then that method call is happening on the background thread, too. – Ken Thomases Aug 09 '14 at 18:24
  • Ken, sorry for the terminology, not as familiar with Objective C Cocoa as I should be. I am doing performSelectorOnMainThread... The code is set up as follows: – Bob Fields Aug 09 '14 at 19:23
  • You're using `-performSelector` **`InBackground`** `:`, not **`OnMainThread`**. That's sort of the opposite of what I'm saying. You run some work in the background, which is fine so long as it doesn't interact with the GUI. If there's something that has to update the GUI, then you need to shunt it to the main thread. – Ken Thomases Aug 09 '14 at 20:24
  • I'm confused, are you saying that the call to appController.m's removeEntity( ) from the background songPlayer.m interacts with the GUI outside the mainthread? Isn't appController run on the main thread? Changing the selector to OnMainThread locks the GUI until the queue is finished whether I use waitUntilDone:YES || NO. PerformSelector for the code within the try {} on the main thread, is that what you are saying? – Bob Fields Aug 09 '14 at 21:16
  • As I said above, objects don't live on specific threads. No, appController does not (just) run on the main thread. If you invoke a method on the appContoller in a background thread, that's where it's invoked. Right there, in that background thread. – Ken Thomases Aug 09 '14 at 21:20

1 Answers1

0

In your -previewSong: method, you do:

        [_myPlayer performSelectorInBackground:@selector(playMyQueue:)
            withObject:activeQueue];

So, -playMyQueue: runs on a background thread. It then does:

[self.delegate songPlayer:self removeEntity:entity];

Since you haven't done anything to shunt this work to another thread, it's happening on the same background thread as the rest of -playMyQueue:. Likewise, the manipulation of the array controller that's done in -songPlayer:removeEntity: is done on the background thread and, since the GUI is bound to that array controller, that's updating the GUI from a background thread, which won't work reliably.

You can fix this by doing:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.delegate songPlayer:self removeEntity:entity];
});

Beyond the issue of updating the GUI from the background thread, you have the issue of general thread-safety. You must synchronize access to shared data structures from multiple threads. For example, if any thread may mutate the contents of a mutable array, then it must have exclusive access to the array for the duration of its mutation. You can't even have other threads reading the array, because they may access it while it's in an indeterminate state in the middle of being modified. It is safe to have multiple threads simultaneously reading from an array so long as none of them mutate it.

This applies to both the content array of the array controller and your _seqFile array.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • Ken, thank you so much; it is scary how much I do not know. I changed the command for both slider and collection view and both are working on my initial tests. I've begun to read about threads on OSX at Apple.com, to get a better grip. Again, thanks for your help. – Bob Fields Aug 09 '14 at 22:05