I've a messages app and I started to create a widget.
Updating the core data with the new messages happens when user open the app.
My wish is when:
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler
called I will get the UIViewController and call the my get messages thread.
Linking the UIViewController against my widget target gave me an error:
'sharedApplication' is unavailable....
So I canceled it.
What I'm trying to achieve:
1. widgetPerformUpdateWithCompletionHandler is being called
2. Application start the get messages thread/method
3. when it finish, it send back data to the widget using NSUserDefaults
My code:
1:
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler
{
// Perform any setup necessary in order to update the view.
[self startGetMessages];
// If an error is encountered, use NCUpdateResultFailed
// If there's no update required, use NCUpdateResultNoData
// If there's an update, use NCUpdateResultNewData
completionHandler(NCUpdateResultNewData);
}
2:
- (void)startGetMessages
{
NSLog(#"%s", __PRETTY_FUNCTION__);
NSBundle *deviceBundle = [NSBundle mainBundle];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:deviceBundle];
id MainController = [storyboard instantiateViewControllerWithIdentifier:#"MainTableViewController"];
SEL getMessagesSelector = NSSelectorFromString(#"startGetMessages:");
if (MainController)
{
NSThread *startGetMessagesThread = [[NSThread alloc] initWithTarget:MainController
selector:getMessagesSelector
object:StringForInt(HRTableDataSourceKindUpdate)];
[startGetMessagesThread start];
}
}
3:
- (void)notifyWidgetForChanges
{
__block NSMutableDictionary *newMessages = [NSMutableDictionary new];
NSArray *results = [CoreDataPhotoRecord MR_findAllSortedBy:#"message.originalDate"
ascending:NO
withPredicate:[NSPredicate predicateWithFormat:#"(message.delete_message == %#) AND (message.type.integerValue == %d) AND (message.originalDate >= %#)",
#NO, NORMAL_MESSAGE, _notiftWidgetDate]];
NSLog(#"%s, _notiftWidgetDate: %#, newMessages.count: %d", __PRETTY_FUNCTION__, _notiftWidgetDate, newMessages.count);
[results enumerateObjectsUsingBlock:^(CoreDataPhotoRecord *photoDetails, NSUInteger idx, BOOL *stop)
{
if (photoDetails != nil && photoDetails.message != nil)
{
NSString *cleanMobile = [[ABAddressBook sharedAddressBook] getCleanMobile:photoDetails.message.mobile];
Contact *person = [[ABAddressBook sharedAddressBook] findContactWithPhoneNumber:cleanMobile];
ContactWidget *contact = [[ContactWidget alloc] init];
contact.name = (person != nil && person.name != nil && person.name.length > 0) ? person.name : cleanMobile;
[newMessages setObject:contact forKey:cleanMobile];
}
}];
[SharedUtilities archiveObject:newMessages.copy forKey:MESSAGES_KEY_NEW widget:true];
[DEFAULTS_WIDGET setObject:#"111" forKey:#"111"];
[DEFAULTS_WIDGET synchronize];
newMessages = nil;
results = nil;
}
widgetDefaults = [[NSUserDefaults alloc] initWithSuiteName:WIDGET_GROUP_NAME];
Nothing is happen since the MainController in step 2 is nil.
What can I do?
The nil problem occurs because you try to access application's storyboard from widget. It's not straightforward, since the containing app and widget extension are being kept in a separate bundles. So the [NSBundle mainBundle] in step 2) is not the same bundle as the one in your app.
Possible solutions include:
including the app's Main.storyboard in extensions bundle either via adding it to Copy Bundle resources list at widget's target Build Phases tab or just adding widget target to Main.storyboard list of Target Membership
moving the code responsible for getting the messages from MainController startGetMessages: into a shared framework that will be accessible both from the app and the widget, preferably into a dedicated object.
The second one is way better. As a rule of thumb it's best to follow SOLID principles when doing the object-oriented programming, where S stands for single responsibility. It should not be a responsibility of view controller to provide the messages fetching system-wide. Creating a dedicated object that will have only one job - to get messages - and sharing it across the targets is a way to go.
Please consult the docs for the detailed explanation on how to create the shared framework: https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1
Related
So far the remote wipe works, but I'm having trouble starting a backup in the background. The remote wipe also works in the background. I am trying to call the startBackgroundBackupActivity method from my locationHandler class which works in the background.
BackgroundBackupHandler.m
- (void) OnSyncComplete:(NSNumber*)result message:(NSString *)message{
NSLog(#"-(void)OnSyncComplete:%# message=%#",result, message);
//jxxtodo: Ensure all existing objects are reset, including DB and network connections
if (0 == [result intValue]){
NSString *strMsg = [ErrorHandler getErrorTextByErrorNumber:SYNC_SUCCESS_INF withObjects:nil];
[self logEvent:strMsg];
}else if (2 == [result intValue]){//no sync required
NSString *strMsg = [ErrorHandler getErrorTextByErrorNumber:NO_NEED_SYNC_INF withObjects:nil];
[self logEvent:strMsg];
}else if (3 == [result intValue]) {
NSString *strMsg = [ErrorHandler getErrorTextByErrorNumber:SYNC_RESET_EMPTY_INF withObjects:nil];
[self logEvent:strMsg];
} else{
NSString *strMsg = [ErrorHandler getErrorTextByErrorNumber:SYNC_COMMON_ERR withObjects:nil];
[self logEvent:strMsg];
return;
}
[m_pSyncController release];
m_pSyncController = nil;
self.m_backupSet = nil;
[self performSelector:#selector(startBackgroundBackupActivity) withObject:nil afterDelay:1.5];
}
Right now, OnSyncComplete:message: is what calls startBackupActcitivy from within the BackgroundBackupHandler class.
I have another class LocationHandler which checks the flag sent from the server and does something based on the flag. So if the flag is set to backup then wipe, it will run a backup then wipe the device.
How would I call OnSyncComplete:message: from the LocationHandler class.
Ive tried:
BackgroundBackupHandler *bgBackup = [[BackgroundBackupHandler alloc]init];
[bgBackup OnSyncComplete:[NSNumber numberWithInt:3] message:nil];
This is giving me errors and terminating my application. Is there anyway to call startBackgroundBackupActivity from the LocationHandlerClass.
The error is that the application crashed and aborts. NSInvalidArgumentException, where nil is not a legal NSManagedObjectContext.
The locationHandler will start a background task, which will then sync the device and return control to the OnSyncComplete method in the LocationHandler class which then in turn calls the OnSyncComplete in the BackgroundBackupHandler class.
The 2 ways to create communication between object are:
1) delegations
2) notifications
In your case it seems like the notification can work better.
Check this out
I'm developing an Apple Watch App, and I need to notify the watch when certain changes occur in the parent application. I'm using the MMWormhole library found on GitHub, but I'm having trouble passing messages from the phone to the watch. Here is my code, do you have any ideas on why this is happening?
My main viewController code looks like this
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"com.mycompany.myapp"
optionalDirectory:#"wormhole"];
NSString *myString = [[NSString alloc] initWithFormat:#"Test String"];
[self.wormhole passMessageObject:#{#"string" : myString}
identifier:#"messageIdentifier"];
My InterfaceController from my WatchkitExtension looks like this:
InterfaceController.m
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
// Initialize the wormhole
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"com.mycompany.myapp"
optionalDirectory:#"wormhole"];
// Obtain an initial value for the selection message from the wormhole
id messageObject = [self.wormhole messageWithIdentifier:#"messageIdentifier"];
NSString *string = [messageObject valueForKey:#"string"];
if (string != nil) {
NSLog(string);
[myLabel setText:string];
}
// Listen for changes to the selection message. The selection message contains a string value
// identified by the selectionString key. Note that the type of the key is included in the
// name of the key.
[self.wormhole listenForMessageWithIdentifier:#"messageIdentifier" listener:^(id messageObject) {
NSString *string = [messageObject valueForKey:#"string"];
if (string != nil) {
[self.myLabel setText:string];
}
}];
}
Thank you!
Is "com.mycompany.myapp" the real value you use in the app? Because group identifiers have to start with group..
If you use a wrong group identifier everything fails because the containerURLForSecurityApplicationGroupIdentifier call inside MMWormhole returns nil. Unfortunately the developers of MMWormhole didn't do any checks or asserts to make sure that the shared group identifier is correct.
So I would recommend to stop concentrating on MMWormhole for a minute. Instead add this code early in your code (e.g. applicationDidFinishLaunching) to verify that your container identifier is correct:
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSURL *appGroupContainer = [fileManager containerURLForSecurityApplicationGroupIdentifier:#"group.com.mycompany.myapp"];
if (!appGroupContainer) {
NSLog(#"group identifier incorrect, or app groups not setup correctly");
}
This will tell you if your app group setup is incorrect.
I'm not sure how far you are into setting up app groups, but you have to use the group identifier you used in the App Groups capabilities section of your project.
I'm using Nico Kreipke's FTPManager (click here to go to GiHub) to download some data from an FTP address.
The code works if it's run before the user's first interaction, after that it will usually fail (about 9 out of 10).
When it fails, the following message is written (0x_ are actually valid addresses):
request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_
That message isn't written by neither my code nor by FTPManager, but by Apple's. On its GitHub, I've found some one with the same error, but the source of it could possible be the same as mine. (That person wasn't using ARC.)
If I try to print the objects of those addresses with the pocommand, the console writes that there's no description available.
Also, the memory keeps adding up until the app receives a memory warning, and soon after the OS terminates it.
By pausing the app when that message appears, I can see that the main thread is in a run loop.
CFRunLoopRun();
The Code
self.ftpManager = [[FTPManager alloc] init];
[self downloadFTPFiles:#"192.168.2.1/sda1/1668"];
ftpManageris a strong reference.
The downloadFTPFiles: method:
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer* server = [FMServer serverWithDestination: basePath username:#"test" password:#"test"];
NSArray* serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary * sDataI = serverData[i];
NSString* name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber* type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString * nextDestination = [basePath stringByAppendingPathComponent: name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
[self.ftpManager downloadFile:name toDirectory:[NSURL fileURLWithPath:NSHomeDirectory()] fromServer:server];
}
}
}
What I've Done
I've tried running that code on several places:
The app delegate's application:didFinishLaunchingWithOptions:;
The viewDidLoad, viewWillAppear: and viewDidAppear: of the a view controller loaded just after the app launches and a view controller presented later.
By an action triggered with a button event.
The download of the data is always well performed when executed by the delegate or a view controller loaded with the app (with an exception). But when run after the user's first interaction with the app, it'll most likely fail with the mentioned error.
The exception for view controllers loaded before the user's first interaction is when the call is in either the viewWillAppear: or viewDidAppear: methods. When it's called a second time (for example, a tab of a tab bar controller) it'll also, most likely, fail.
The Question
Does anyone have an idea of what may be happening, or if I'm doing something wrong? Or any alternative solution, maybe?
Any help to solve this problem will be welcomed.
Thanks,
Tiago
I ended up sending the downloadFile:toDirectory:fromServer: message inside a dispatch_async block. I've also created an FTPManage for every file downloaded.
It worked, but I have no idea why.
I'm leaving this answer to whomever crosses with this problem.
If anyone can let me know why this technique worked, please comment bellow so I can update the answer.
Here's the new way I'm downloading each file:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
Again, If you know why this worked, let me know.
Thanks.
Full Method
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer *server = [FMServer serverWithDestination:basePath username:#"test" password:#"test"];
NSArray *serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary *sDataI = serverData[i];
NSString *name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber *type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString *nextDestination = [basePath stringByAppendingPathComponent:name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
}
}
}
I have created a FacebookManager singleton that gets called on a background thread when my app launches. Everything is working just fine with the facebook manager the singleton, the app etc. However, when the app first launches, it is quite a few seconds before it is useful because the facebook manager has not finished doing its thing yet. So what I want to do, is use NSKeyedArchiver to save the facebookManager and all its dictionaries so that upon launch, the app has a navigable interface while the facebook data is being updated in the background. Make sense?
All within the FacebookManager.m, first, when the manager is done updating the friends dictionaries, etc, I call the method that saves the data:
- (BOOL)saveFacebookData
{
// returns success or failure
NSString *path = [self archivePath]; // just a helper method
return [NSKeyedArchiver archiveRootObject:self toFile:path];
}
Then in init, I am trying this, which doesn't seem to work. :
-(id)init
{
self = [super init];
NSString *path = [self archivePath];
self = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
// If the manager hadn't been saved previously, create a new new one
if (!self) {
if (_idsByNameDict == nil) {
_idsByNameDict = [[NSMutableDictionary alloc] init];
}
if (_photosByNameDict == nil) {
_photosByNameDict = [[NSMutableDictionary alloc] init];
}
if (_installedByNameDict == nil) {
_installedByNameDict = [[NSMutableDictionary alloc] init];
}
if (_allFriendsArray == nil) {
_allFriendsArray = [[NSArray alloc] init];
}
basicPermissions = NO;
extendedPermissions = NO;
// Create synchronous dispatch queue for all facebook activity
if (_facebookUpdateQueue == nil) {
_facebookUpdateQueue = dispatch_queue_create("com.facebookUpdateQueue", NULL);
}
}
I think my general strategy is sound but I am tripping over how to actually grab the archived version of the manager during init! Any advice?
Your class needs to implement <NSCoding> and both of its methods encodeWithCoder: to archive all of your property values and initWithCoder: to in archive them. Make sure to call super in the implementations. Generally, the class using the archived class would know about the archiving but you could hide that knowledge in init by using initForReadingWithData: to create your NSKeyedUnarchiver and then calling [self initWithCoder:...];.
Analyzer keeps saying that I have a leak in the line with the * at the beginning and end, how would I fix this leak so it gets rid of the warning?
+ (void)flushOfflineQueue
{
// TODO - if an item fails, after all items are shared, it should present a summary view and allow them to see which items failed/succeeded
// Check for a connection
if (![self connected])
return;
// Open list
NSMutableArray *queueList = [self getOfflineQueueList];
// Run through each item in the quietly in the background
// TODO - Is this the best behavior? Instead, should the user confirm sending these again? Maybe only if it has been X days since they were saved?
// - want to avoid a user being suprised by a post to Twitter if that happens long after they forgot they even shared it.
if (queueList != nil)
{
SHK *helper = [self currentHelper];
if (helper.offlineQueue == nil)
***helper.offlineQueue = [[NSOperationQueue alloc] init];***
SHKItem *item;
NSString *sharerId, *uid;
for (NSDictionary *entry in queueList)
{
item = [SHKItem itemFromDictionary:[entry objectForKey:#"item"]];
sharerId = [entry objectForKey:#"sharer"];
uid = [entry objectForKey:#"uid"];
if (item != nil && sharerId != nil)
[helper.offlineQueue addOperation:[[[SHKOfflineSharer alloc] initWithItem:item forSharer:sharerId uid:uid] autorelease]];
}
// Remove offline queue - TODO: only do this if everything was successful?
[[NSFileManager defaultManager] removeItemAtPath:[self offlineQueueListPath] error:nil];
}
}
Thanks!
When you use properties they will often perform the proper memory management. In your situation you need to autorelease the class you set.
helper.offlineQueue = [[[NSOperationQueue alloc] init] autorelease];