Perform search through array in background thread? - objective-c

I have a pretty large array I get from my database peopleArray that consists of all the users of my app. This array is used for searching for friends. My problem is, when the user begins to type in the search bar the app often freezes for a moment before display the searched user.
#pragma mark - SEARCH BAR
- (void) filterContententForSearchText: (NSString *) searchText scope:(NSString *) scope{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%K beginsWith[cd] %#",#"Name", searchText ];
self.searchArray = [self.peopleArray filteredArrayUsingPredicate:predicate];
}
- (BOOL) searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
[self filterContententForSearchText:searchString scope:[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];
return YES;
}
- (void) searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller{
[self.tableView reloadData];
}
I would like to do this in the background so I can put a UIActivityIndicator in the tableView as it loads but am not sure where or how to implement which method in the background.

First of all, I would suggest using a timer so that you don't reload the users on EVERY keypress. I do that like this:
//I put this in my private #interface
#property (nonatomic, strong) NSTimer *searchTimer;
//Then we have the method called on keypress
- (void)whateverMethodIsCalledOnKeypress {
[self.searchTimer invalidate];
self.searchTimer = nil;
//put some logic that returns out of the function for empty strings etc. here
self.searchTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(runSearch) userInfo:nil repeats:NO];
}
- (void)runSearch {
//do whatever you need to run the search this way
//it's only ever done at most once per second
//so fast typists don't overload the processor
}
And here is some code to do an async filtering.
//Show your activity indicator
dispatch_async(dispatch_get_global_queue(0,0), ^{
//call whatever you need to do on the filtering here
dispatch_async(dispatch_get_main_queue(), ^{
//Hide your activity indicator
[self.tableView reloadData];
});
});

Related

how can I make clickable url in NSTableView

I am trying to build OS-X core data based app. In one of the entities, I am storing an URL ex. (www.somesite.com/somepage/someindex.php)
Using binding, I am successfully displaying the URL in the NSTableView. I would like however that URL to be clickable, and when clicked, browser to fire up and open the page. I have done some research, and I have found some solutions, for example:
Clickable url link in NSTextFieldCell inside NSTableView?
also:
https://developer.apple.com/library/mac/qa/qa1487/_index.html
but they both look outdated, first one is six years old, while the second is last updated on Jan. 2005
Anyone can provide easier & faster way how to achieve this? I didn't expected that I will have to write bunch of code just to make simple link to work to be honest... I am coming from web development world, where those kind of things can be sorted out withing few seconds, while here seems to be totally different story....
Any help will be appreciated.
John
You can use NSTextView and implement its delegate. There is a demo:
// MyCellView.h
#interface MyCellView : NSView
#property (nonatomic, strong) IBOutlet NSTextView *textView;
#end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.delegate = self;
self.tableView.dataSource = self;
NSNib *nib = [[NSNib alloc] initWithNibNamed:#"MyCellView" bundle:[NSBundle mainBundle]];
[self.tableView registerNib:nib forIdentifier:#"MyCell"];
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
MyCellView *cell = (MyCellView *)[tableView makeViewWithIdentifier:#"MyCell" owner:self];
cell.textView.delegate = self;
[cell.textView.textStorage setAttributedString:[self makeLinkAttributedString:#"This is a test: www.somesite.com/somepage/someindex.php"]];
return cell;
}
- (NSAttributedString *)makeLinkAttributedString:(NSString *)string {
NSMutableAttributedString *linkedString = [[NSMutableAttributedString alloc] initWithString:string];
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
[detector enumerateMatchesInString:string options:0 range:NSMakeRange(0, string.length) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
if (match.URL) {
NSDictionary *attributes = #{ NSLinkAttributeName: match.URL };
[linkedString addAttributes:attributes range:match.range];
}
}];
return [linkedString copy];
}
#pragma mark - NSTextViewDelegate methods
- (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex {
// The click will be handled by you or the next responder.
return NO;
}
You can use TTTAttributedLabel in your tableviewcell. It supports powerful link detection.

Not understanding how UIActivityIndicatorView works with NSTimer

I'm missing something about how UIActivityIndicatorView and NSTimer work together.
I've added this UIActivityIndicatorView in Interface Builder with the following settings:
The UIWebView is instantiated as self.webV and the UIActivityIndicatorView as self.indicator.
I have the following code in the implementation file:
-(void)viewDidLoad
{
[super viewDidLoad];
//Create UIWebView.
if (!self.webV)
{
self.webV = [[UIWebView alloc] init];
}
self.webV.delegate = self;
//Load web page.
NSString *baseURLString = #"myURL.com";
NSString *urlString = [baseURLString stringByAppendingPathComponent:#"myURL.com"];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0/2.0 target:self selector:#selector(timerLoad) userInfo:nil repeats:YES];
[self connectWithURL:urlString andBaseURLString:baseURLString];
}
-(void)timerLoad
{
if (!self.webV.loading)
{
[self.indicator stopAnimating];
}
else
{
[self.indicator startAnimating];
}
}
But when the UIWebView loads, no activity indicator shows up. What am I doing wrong or leaving out?
Thanks for the help, folks.
I'm really not sure on what the behaviour of the UIActivityIndicatorView is supposed to be if you repeatedly call start/stop on it. I am reasonably sure it isn't meant to be used that way :)
So, even though your question is specific to NSTimer and UIActivityIndicatorView, it may be helpful to understand that you should approach your solution differently.
Instead of using a timer that repeatedly calls [self.indicator startAnimating] every half-second, you should use the webview delegate methods to toggle the UIActivityIndicatorView on and off.
-(void)viewDidLoad
{
[super viewDidLoad];
//Create UIWebView.
if (!self.webV)
{
self.webV = [[UIWebView alloc] init];
}
self.webV.delegate = self;
//Load web page.
NSString *baseURLString = #"myURL.com";
NSString *urlString = [baseURLString stringByAppendingPathComponent:#"myURL.com"];
//self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0/2.0 target:self selector:#selector(timerLoad) userInfo:nil repeats:YES];
[self connectWithURL:urlString andBaseURLString:baseURLString];
}
- (void)webViewDidStartLoad:(UIWebView *)webView{
//start animating
[self.indicator startAnimating];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//stop animating
[self.indicator stopAnimating];
}
There could be several reasons. It could load so fast that the loading is already done, or it never loaded at all because something is wrong with the URL.
This isn't the cause of your problem, but you never invalidate your timer, which you should.
I was also going to make the point that you should use delegate methods instead of a timer, but pdriegen beat me to it.

Can NSTableView column bound to coredata Entity NSArrayController in secondary thread work?

My program uses Coredata (SQLite), NSPersistentDocument, NSTableView and an (entity) NSArrayController. I want to have the NSTableView's columns in the Main thread bound to the entity NSArrayController that I have populated in a Secondary Thread.
Question 1: Is it possible?. Unfortunately is not working in my case (while doing everything in the same thread through IB works)
What's the objective: let the "fetch's" (big document average is 2-4 secs to finish) run in a secondary thread so I can show a progress indicator on the UI while fetching.
Question 2: Is there any other recommended way os showing a progress indicator while the entity nsarraycontroller is arranging its data, fetching, etc...?
Thanks in advance.
Luis
// ------- ABCoredataController.h
#interface ABCoredataController : NSObject {
:
NSArrayController *ivArrayController;
}
#property (nonatomic, assign) NSArrayController *arrayController;
// ------- ABCoredataController.m
// This piece executes in Main thread...
- (void) init {
ivArrayController = [[NSArrayController alloc] init];
:
// Following is later executed in the Secondary Thread
- (void) secondaryThreadRun:(id)param {
:
// prepare everything to access coredata from a secondary thread...
[self setSecondaryThreadMOC: [[[NSManagedObjectContext alloc]init] autorelease] ];
[[self secondaryThreadMOC] setPersistentStoreCoordinator:[self mainThreadPSC]];
// prepare the (entity) array controller
[[self arrayController] setAvoidsEmptySelection:YES];
[[self arrayController] setPreservesSelection:YES];
[[self arrayController] setSelectsInsertedObjects:YES];
[[self arrayController] setClearsFilterPredicateOnInsertion:YES];
[[self arrayController] setAutomaticallyPreparesContent:YES];
[[self arrayController] setAutomaticallyRearrangesObjects:YES];
[[self arrayController] setAlwaysUsesMultipleValuesMarker:NO];
[[self arrayController] setUsesLazyFetching:NO];
[[self arrayController] setEditable:YES];
[[self arrayController] setEntityName:#"Transaction"];
// bind arrayController to the managedObjectContext
[[self arrayController] setManagedObjectContext:[self secondaryThreadMOC]];
[[self arrayController] setFilterPredicate:[self predicate]];
:
Then inside the class where I control my XIB and all the UI...
// ------- ABWindowController.m
:
// Start the secondaryThreadRun in previous class
[[self coredataCtrlTransaction] start];
// Get the pointer to the entity array controller !!! <== HERE!! is it right?
ivOut_CtEn_Transaction = [[self coredataCtrlTransaction]arrayController];
:
// Bind that entity array controller to the NSTableView columns...
if ( [self out_CtEn_Transaction] != nil ) {
for ( NSTableColumn *column in [[self out_Tableview_Transaction] tableColumns] ) {
if ( [column identifier] != nil ) {
if ( [column infoForBinding:#"value"] == nil ) {
NSString *theKeyPath=nil;
if ( [[column identifier] length] > 4 )
theKeyPath = [[column identifier] substringFromIndex:4];
else
theKeyPath = [column identifier];
[column bind: #"value" toObject: [self out_CtEn_Transaction]
withKeyPath:[NSString stringWithFormat:#"arrangedObjects.%#", theKeyPath] options:nil];
}
}
}
}
Answering myself, I've discovered that KVO is no good for interthread communication, I'm setting the observer in the MainThread, but the observation is received on the thread that originates the key value change (Secondary thread where the nsarraycontroller lives).
So, if my background thread changes a value, my background thread is going to receive the KVO about it. Which is something I don't want.
Found good comment about it here:
I've found another way of achieving my objective, much more simple.
Found very good examples on GDC here and here
My objective was: let me show an spinning wheel while my "nsarraycontroller is fetching or arranging is objects", which in my case means 2-3 seconds.
// Last step in the preparation of the predicate
NSPredicate *predicadoFinal = nil;
predicadoFinal = [NSCompoundPredicate andPredicateWithSubpredicates:array_AND_Total];
// Use GCD (Grand Central Dispatch) to be able to show the spinning wheel
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
// Do here my stuff of showing in the UI something...
[[self out_ABSpinning_DDBB] setHidden:NO];
[[self out_ABSpinning_DDBB] startAnimation:self];
});
// And here CPU consuming stuff.
// Apply the predicate to the nsarraycontroller.
// As the controller has both setAutomaticallyPreparesContent:YES
// and setAutomaticallyRearrangesObjects:YES so setting
// the predicate will automatically trigger the controller
// to process the predicate and fetch again...
[self setPredicateFINAL:predicadoFinal];
});
How do I stop the spinning wheel. It's also easy, I've setup an observer on the entity NSArrayController like this:
if (nil == ivObservableKeysABCtEn_Transaction ) {
ivObservableKeysABCtEn_Transaction = [[NSSet alloc] initWithObjects:
#"arrangedObjects",
nil];
}
:
if ( [self out_CtEn_Transaction] != nil ) {
for (NSString *keyPath in [self observableKeysABCtEn_Transaction]) {
// Añado observers para cada uno de los keyPaths en los que estoy interesado
[[self out_CtEn_Transaction] addObserver:self
forKeyPath:keyPath
options:0
context:ABObserverABCtEn_Transaction];
}
}
And then in:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Fetch del objeto que ha cambiado en el emisor. En mi caso un BOOL
id newChange = [change objectForKey:NSKeyValueChangeNewKey];
// Detect null's
if ([NSNull null] == (NSNull*)newChange) {
newChange = nil;
} else {
:
//
// Somthing has changed in the "arrangedObjects" property
// of my coredata array controller, so it has definitely
// finished doing its work.
if ( context == ABObserverABCtEn_Transaction ) {
[[self out_ABSpinning_DDBB] stopAnimation:self];
[[self out_ABSpinning_DDBB] setHidden:YES];
Thanks
Luis

Check if an animation image is in an image view at a given time

I have an image view that has two animation images, occuring in 1-second intervals. I want to run some methods when my image view is displaying one of those two images
I already tried doing:
if(self.imageViewThatPerformsAnimation.image == [UIImage imageNamed: #"someImage"])
[self doSomeMethod];
but when I tried this and ran it, [self doSomeMethod]; always ran, and not just when the image view was displaying that one image.
I'm thinking about having a timer that changes a boolean value every one second then saying
if (booleanValue==YES)
[self doSomeMethod]
It's just that I feel there may be a better way.
If you wanted to use a NSTimer, it might look like:
#interface MyViewController ()
{
NSTimer *_timer;
NSArray *_images;
NSInteger _currentImageIndex;
}
#end
#implementation MyViewController
#synthesize imageview = _imageview;
- (void)viewDidLoad
{
[super viewDidLoad];
_images = [NSArray arrayWithObjects:
[UIImage imageNamed:#"imgres-1.jpg"],
[UIImage imageNamed:#"imgres-2.jpg"],
[UIImage imageNamed:#"imgres-3.jpg"],
[UIImage imageNamed:#"imgres-4.jpg"],
nil];
_currentImageIndex = -1;
[self changeImage];
// Do any additional setup after loading the view.
}
- (void)changeImage
{
_currentImageIndex++;
if (_currentImageIndex >= [_images count])
_currentImageIndex = 0;
self.imageview.image = [_images objectAtIndex:_currentImageIndex];
if (_currentImageIndex == 0)
[self doSomething];
}
- (void)startTimer
{
if (_timer) {
[_timer invalidate];
_timer = nil;
}
_timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:#selector(changeImage)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}
- (void)stopTimer
{
[_timer invalidate];
_timer = nil;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self startTimer];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self stopTimer];
}

Receiving Drag events on an NSWindow when the window contains a single WebView view

I am new to objective-c and cocoa so please break things down for me. I'm working on a project started by another developer and have only been working in objective-c for 3 days.
I have an NSWindow subclass that contains a WebView view. The WebView content that is loaded is a Silverlight plugin. I registered the NSWindow to receive Drag events. The drag events are being generated but only when the drag occurs within the NSWindow Title Bar. I register for the drag events in the load method.
AdminWindow.mm
#import "AdminWindow.h"
#import "NativeMessageReceiver.h"
extern AdminWindow* adminRiaWindow;
#implementation AdminWindow
#synthesize adminWebView;
BOOL isAdminContentLoaded;
-(void) load
{
if (!isAdminContentLoaded)
{
NSLog(#"loading Admin window");
NSString *curDir = [[NSBundle mainBundle] bundlePath];
NSString* url = [NSString stringWithFormat: #"file://%#/Contents/Resources/RIA/AdminContentMac.html",curDir];
[[adminWebView mainFrame] loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: url]]];
[adminWebView setDrawsBackground:NO];
id win = [adminWebView windowScriptObject];
NativeMessageReceiver* receiver = [NativeMessageReceiver getInstance];
[win setValue:receiver forKey:#"NativeMessageReceiver"];
receiver.adminWebView = adminWebView;
isAdminContentLoaded = YES;
}
}
-(void) show
{
[self load];
[self setIsVisible: YES];
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
[self makeKeyAndOrderFront: self];
[self makeMainWindow];
[self center];
}
-(void) hide
{
[self setIsVisible: NO];
}
- ( BOOL ) windowShouldClose : ( id ) sender
{
[self setIsVisible: NO];
return NO;
}
- (BOOL) canBecomeKeyWindow
{
return YES;
}
- (BOOL) canBecomeMainWindow
{
return YES;
}
#end
extern "C" void ShowAdminWindow()
{
NSLog(#"showing Admin window");
if (![NSThread isMainThread])
[adminRiaWindow performSelectorOnMainThread:#selector(show) withObject:nil waitUntilDone:YES];
else
{
[adminRiaWindow show];
}
}
extern "C" void HideAdminWindow()
{
if (![NSThread isMainThread])
{
[adminRiaWindow performSelectorOnMainThread:#selector(hide) withObject:nil waitUntilDone:YES];
}
else
{
[adminRiaWindow hide];
}
}
CustomeWebView
#implementation SteamPunkWebView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]];
NSLog(#"In Custom View Init");
}
return self;
}
I think you're only getting drag events in the title bar because you registered the window to receive the events, not the web view.
Try this instead:
[self.adminWebView registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]];
WebViews are one of the most complex views in Cocoa and are a special case when it comes to dragging. Instead of implementing the standard NSView dragging behaviour, you need instead to set an object as the web view's WebUIDelegate and implement the following delegate method:
‑webView:dragDestinationActionMaskForDraggingInfo:
You can then control how you want the web view to respond to the drag action. The WebUIDelegate protocol has several other methods to control dragging, so you should read the documentation for more detail.
To actually receive the dragged objects and process them, you will need to subclass WebView and implement ‑draggingEnded:.
You definitely do not need to add any dragging methods to the window itself.