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.
Related
I'm working on voip app. fetch contact work but when I want to make call, app crash.
[ABSAddressBook contacts]: message sent to deallocated instance
0x1c1478180 warning: could not execute support code to read
Objective-C class data in the process. This may reduce the quality of
type information available.
crash happen in this line.
NSArray *lContacts = (NSArray *)ABAddressBookCopyArrayOfAllPeople(addressBook);
- (void) checkContactListForJogvoiceList {
// if (![BundleLocalData isLoadingJogvoiceContactList]) {
// [BundleLocalData setLoadingJogvoiceContactList:true];
int maxPhoneNumberSubmit = 200;
NSArray *lContacts = (NSArray *)ABAddressBookCopyArrayOfAllPeople(addressBook);
NSMutableDictionary *phoneNumberContactsDictionary = [[NSMutableDictionary alloc] init];
NSMutableArray *allPhoneNumberList = [[NSMutableArray alloc] init];
for (id lPerson in lContacts) {
ABRecordRef person = (ABRecordRef)lPerson;
NSArray *phoneList = [AppUtil getContactPhoneList:person];
for (NSString* phoneNumber in phoneList) {
NSMutableArray* contactList = phoneNumberContactsDictionary[phoneNumber];
if (!contactList) {
contactList = [[NSMutableArray alloc] init];
}
[contactList addObject:(__bridge ABRecordRef)person];
phoneNumberContactsDictionary[phoneNumber] = contactList;
}
[allPhoneNumberList addObjectsFromArray:phoneList];
if (allPhoneNumberList.count >= maxPhoneNumberSubmit) {
[self checkContactList:allPhoneNumberList phoneNumberContactsDictionary:phoneNumberContactsDictionary];
}
}
if (allPhoneNumberList.count > 0) {
[self checkContactList:allPhoneNumberList phoneNumberContactsDictionary:phoneNumberContactsDictionary];
}
// ABAddressBookUnregisterExternalChangeCallback(addressBook, sync_address_book, self);
// [BundleLocalData setLoadingJogvoiceContactList:false];
// }
}
probably because AddressBook framework deprecate in ios9? am I right?
I don’t want to use Contacts framework.
According to Apple doc ABAddressBookCreateWithOptions using address book function ABAddressBookCreateWithOptions returns NULL if no permission granted from user.
On iOS 6.0 and later, if the caller does not have access to the Address Book database:
For apps linked against iOS 6.0 and later, this function returns NULL.
For apps linked against previous version of iOS, this function returns an empty read-only database.
You should follow this article How do I correctly use ABAddressBookCreateWithOptions method in iOS 6?
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
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:...];.
My application is involves users saving data which I store using NSCoding/NSKeyedArchiver. I supply the user with sample data objects on the first run of the app.
The expected behavior happens during regular testing, as well as through ad hoc deployment. Unfortunately, a significant bug happens when the app is downloaded through the app store.
What other environmental considerations might there be so I can reproduce (and then fix) the issue in my regular testing?
Expected behavior:
A new user may add/edit data objects in addition to the current ones. (A classic CRUD scenario).
Actual behavior:
If the user's first action is to save a new object, all of the previously loaded sample objects disappear (the elusive bug).
However, If the user's first action is to edit, then all of objects persist as expected, and the user can add additional ones without issue.
Thanks for the help.
EDIT
In my most recent testing, I switched the Build Configuration to release in the "Run " scheme.
http://i.imgur.com/XNyV6.png
App Delegate, which correctly initializes app
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.dataArray = nil;
self.dataArray = [AppDelegate getArray];
if (self.dataArray == nil) {
self.dataArray = [[NSMutableArray alloc] init];
}
//First run of the app
if (dataArray.count == 0) {
//Add sample data to array
//Save array
NSString *path = [AppDelegate getDocPath];
[NSKeyedArchiver archiveRootObject:self.dataArray toFile:path];
}
}
+(NSString *) getDocPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *tempDocPath = [documentsPath stringByAppendingPathComponent:#"FilePath.dat"];
return tempDocPath;
}
+(NSMutableArray *)getArray {
return [[NSKeyedUnarchiver unarchiveObjectWithFile:[AppDelegate getDocPath]] mutableCopy];
}
Object creation, which deletes preloaded data if data hasn't been edited
-(void)viewDidLoad {
tempArray = nil;
tempArray = [NSKeyedUnarchiver unarchiveObjectWithFile:[AppDelegate getDocPath]];
if (tempArray == nil) {
tempArray = [[NSMutableArray alloc] init];
}
}
-(void)saveObject {
[tempArray addObject:createdData];
[tempArray sortUsingSelector:#selector(compare:)];
NSString *path = [AppDelegate getDocPath];
[NSKeyedArchiver archiveRootObject:tempArray toFile:path];
AppDelegate *dg = (AppDelegate *)[[UIApplication sharedApplication] delegate];
dg.dataArray = tempArray;
}
I am not sure how to solve your current problem (without looking at the code), but here's how you can avoid it in the future:
Make sure that the build you submit to the app store is the ad-hoc build you have QA'd, but signed with an app store provisioning profile.
Two advantages:
1) You should be able to repro the same bug on the adhoc and appstore build
2) dSym for both these are the same. So, you dont have to wait to get the AppStore crash logs before you can dig in and see what's happening.
I guess while saving the new object, you are not appending it to the existing data. You might be over-writing the previously created file. You should access the previous file and append the new data to the previous file. Sharing code would help to point out where you are going wrong.
EDIT: Replace the following code and check if its still showing the same behaviour
-(void)viewDidLoad {
tempArray = nil;
tempArray = [NSKeyedUnarchiver unarchiveObjectWithFile:[AppDelegate getDocPath]mutableCopy];
if (tempArray == nil) {
NSLog(#"tempArray is nil"); //if tempArray doesn't get initialized by the file contents
tempArray = [[NSMutableArray alloc] init];
}
}