├── .gitignore ├── README.md └── example-ios ├── .gitignore ├── SampleApp.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── SampleApp ├── AddressBookRecord.h ├── AddressBookRecord.m ├── AddressBookViewController.h ├── AddressBookViewController.m ├── AppDelegate.h ├── AppDelegate.m ├── Base.lproj │ └── Main.storyboard ├── ContactDetailRecord.h ├── ContactDetailRecord.m ├── ContactEditViewController.h ├── ContactEditViewController.m ├── ContactInfoView.h ├── ContactInfoView.m ├── ContactListViewController.h ├── ContactListViewController.m ├── ContactRecord.h ├── ContactRecord.m ├── ContactViewController.h ├── ContactViewController.m ├── CustomNavBar.h ├── CustomNavBar.m ├── GroupAddViewController.h ├── GroupAddViewController.m ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── DreamFactory-logo-horiz-filled.png │ ├── DreamFactory-logo-horiz.png │ ├── LaunchImage.launchimage │ │ └── Contents.json │ ├── chat.png │ ├── default_portrait.png │ ├── home.png │ ├── mail.png │ ├── phone1.png │ ├── skype.png │ └── twitter2.png ├── Info.plist ├── MasterViewController.h ├── MasterViewController.m ├── PickerSelector.h ├── PickerSelector.m ├── PickerSelector.xib ├── ProfileImagePickerViewController.h ├── ProfileImagePickerViewController.m ├── RESTEngine.h ├── RESTEngine.m ├── RegisterViewController.h ├── RegisterViewController.m ├── address_book.png └── main.m ├── SampleAppTests ├── Info.plist └── example_iosTests.m ├── api ├── NIKApiInvoker.h ├── NIKApiInvoker.m ├── NIKFile.h └── NIKFile.m └── package ├── add_ios.dfpkg ├── data.json ├── description.json └── schema.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | SampleApp/TodoList.xcodeproj/project.xcworkspace/xcuserdata/jasonsykes.xcuserdatad/UserInterfaceState.xcuserstate 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Address Book for iOS 2 | ==================== 3 | 4 | This repo contains a sample address book application for iOS Objective-C that demonstrates how to use the DreamFactory REST API. It includes new user registration, user login, and CRUD for related tables. 5 | 6 | #Getting DreamFactory on your local machine 7 | 8 | To download and install DreamFactory, follow the instructions [here](http://wiki.dreamfactory.com/DreamFactory/Installation). Alternatively, you can create a [free hosted developer account](http://www.dreamfactory.com) at www.dreamfactory.com if you don't want to install DreamFactory locally. 9 | 10 | #Configuring your DreamFactory instance to run the app 11 | 12 | - Enable [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) for development purposes. 13 | - In the admin console, navigate to the Config tab and click on CORS in the left sidebar. 14 | - Click Add. 15 | - Set Origin, Paths, and Headers to *. 16 | - Set Max Age to 0. 17 | - Allow all HTTP verbs and check the Enabled box. 18 | - Click update when you are done. 19 | - More info on setting up CORS is available [here](http://wiki.dreamfactory.com/DreamFactory/Tutorials/Enabling_CORS_Access). 20 | 21 | - Create a default role for new users and enable open registration 22 | - In the admin console, click the Roles tab then click Create in the left sidebar. 23 | - Enter a name for the role and check the Active box. 24 | - Go to the Access tab. 25 | - Add a new entry under Service Access (you can make it more restrictive later). 26 | - set Service = All 27 | - set Component = * 28 | - check all HTTP verbs under Access 29 | - set Requester = API 30 | - Click Create Role. 31 | - Click the Services tab, then edit the user service. Go to Config and enable Allow Open Registration. 32 | - Set the Open Reg Role Id to the name of the role you just created. 33 | - Make sure Open Reg Email Service Id is blank, so that new users can register without email confirmation. 34 | - Save changes. 35 | 36 | - Import the package file for the app. 37 | - From the Apps tab in the admin console, click Import and click 'Address Book for iOS' in the list of sample apps. The Address Book package contains the application description, schemas, and sample data. 38 | - Leave storage service and folder blank. This is a native iOS app so it requires no file storage on the server. 39 | - Click the Import button. If successful, your app will appear on the Apps tab. You may have to refresh the page to see your new app in the list. 40 | 41 | - Make sure you have a SQL database service named 'db'. Depending on how you installed DreamFactory you may or may not have a 'db' service already available on your instance. You can add one by going to the Services tab in the admin console and creating a new SQL service. Make sure you set the name to 'db'. 42 | 43 | #Running the Address Book app 44 | 45 | Almost there! Clone this repo to your local machine then open and run the project with Xcode. 46 | 47 | Before running the project you need to edit API_KEY in the file RESTEngine.h to match the key for your new app. This key can be found by selecting your app from the list on the Apps tab in the admin console. 48 | 49 | The default instance URL is localhost:8080. If your instance is not at that path, you can change the default path in RESTEngine.h. 50 | 51 | When the app starts up you can register a new user, or log in as an existing user. Currently the app does not support registering and logging in admin users. 52 | 53 | #Additional Resources 54 | 55 | More detailed information on the DreamFactory REST API is available [here](http://wiki.dreamfactory.com/DreamFactory/API). 56 | 57 | The live API documentation included in the admin console is a great way to learn how the DreamFactory REST API works. 58 | -------------------------------------------------------------------------------- /example-ios/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | 4 | example-ios.xcodeproj/project.xcworkspace/xcuserdata/connorfoody.xcuserdatad/UserInterfaceState.xcuserstate 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData 9 | 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | 22 | ## Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa -------------------------------------------------------------------------------- /example-ios/SampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example-ios/SampleApp/AddressBookRecord.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_AddressBookRecord_h 2 | #define example_ios_AddressBookRecord_h 3 | 4 | #import 5 | 6 | // group model 7 | @interface GroupRecord : NSObject 8 | 9 | @property(nonatomic, retain) NSNumber* Id; 10 | @property(nonatomic, retain) NSString* Name; 11 | 12 | @end 13 | #endif 14 | -------------------------------------------------------------------------------- /example-ios/SampleApp/AddressBookRecord.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "AddressBookRecord.h" 3 | 4 | @implementation GroupRecord 5 | 6 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/AddressBookViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_AddressBookViewController_h 2 | #define example_ios_AddressBookViewController_h 3 | 4 | #import 5 | 6 | @interface AddressBookViewController : UIViewController 7 | 8 | @property (assign, nonatomic) IBOutlet UITableView *addressBookTableView; 9 | 10 | @end 11 | #endif 12 | -------------------------------------------------------------------------------- /example-ios/SampleApp/AddressBookViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AddressBookViewController.h" 4 | #import "ContactListViewController.h" 5 | #import "AddressBookRecord.h" 6 | #import "GroupAddViewController.h" 7 | #import "RESTEngine.h" 8 | #import "AppDelegate.h" 9 | 10 | 11 | @interface AddressBookViewController () 12 | 13 | // list of groups 14 | @property (nonatomic, retain) NSMutableArray *addressBookContentArray; 15 | 16 | // for prefetching data 17 | @property (nonatomic, retain) ContactListViewController* contactListViewController; 18 | @end 19 | 20 | @implementation AddressBookViewController 21 | 22 | - (void) viewDidLoad{ 23 | [super viewDidLoad]; 24 | 25 | self.addressBookContentArray = [[NSMutableArray alloc] init]; 26 | } 27 | 28 | - (void) viewWillDisappear:(BOOL)animated{ 29 | [super viewWillDisappear:animated]; 30 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 31 | CustomNavBar* navBar = bar.globalToolBar; 32 | [navBar.addButton removeTarget:self action:@selector(hitAddGroupButton) forControlEvents:UIControlEventTouchDown]; 33 | } 34 | 35 | - (void) viewWillAppear:(BOOL)animated{ 36 | [super viewWillAppear:animated]; 37 | 38 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 39 | CustomNavBar* navBar = bar.globalToolBar; 40 | [navBar.addButton addTarget:self action:@selector(hitAddGroupButton) forControlEvents:UIControlEventTouchDown]; 41 | [navBar showAdd]; 42 | [navBar showBackButton:YES]; 43 | [navBar enableAllTouch]; 44 | 45 | // refresh the list of groups when the view is coming back up 46 | [self getAddressBookContentFromServer]; 47 | } 48 | 49 | - (void)didReceiveMemoryWarning 50 | { 51 | [super didReceiveMemoryWarning]; 52 | } 53 | 54 | - (void) hitAddGroupButton { 55 | [self showGroupAddViewController]; 56 | } 57 | 58 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 59 | // allow swipe to delete 60 | return YES; 61 | } 62 | 63 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 64 | if (editingStyle == UITableViewCellEditingStyleDelete) { 65 | GroupRecord* record = [self.addressBookContentArray objectAtIndex:indexPath.row]; 66 | 67 | // can not delete group until all references to it are removed 68 | // remove relations -> remove group 69 | // pass record ID so it knows what group we are removing 70 | [self removeGroupFromServer:record.Id]; 71 | 72 | [self.addressBookContentArray removeObjectAtIndex:indexPath.row]; 73 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 74 | } 75 | } 76 | 77 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 78 | return [self.addressBookContentArray count]; 79 | } 80 | 81 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 82 | 83 | static NSString *cellIdentifier = @"addressBookTableViewCell"; 84 | 85 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifier]; 86 | if(cell == nil){ 87 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; 88 | } 89 | GroupRecord* record = [self.addressBookContentArray objectAtIndex:indexPath.row]; 90 | cell.textLabel.text = record.Name; 91 | 92 | cell.textLabel.font = [UIFont fontWithName:@"Helvetica Neue" size: 18.0]; 93 | [cell setBackgroundColor:[UIColor colorWithRed:254/255.0f green:254/255.0f blue:254/255.0f alpha:1.0f]]; 94 | 95 | return cell; 96 | } 97 | 98 | - (void) tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath{ 99 | // fetch data at start of potential press 100 | GroupRecord* record = [self.addressBookContentArray objectAtIndex:indexPath.row]; 101 | self.contactListViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ContactListViewController"]; 102 | self.contactListViewController.groupRecord = record; 103 | [self.contactListViewController prefetch]; 104 | } 105 | 106 | -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 107 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 108 | 109 | [self showContactListViewController]; 110 | } 111 | 112 | - (void) getAddressBookContentFromServer{ 113 | // get all the groups 114 | 115 | [[RESTEngine sharedEngine] getAddressBookContentFromServerWithSuccess:^(NSDictionary *response) { 116 | 117 | [self.addressBookContentArray removeAllObjects]; 118 | 119 | for (NSDictionary *recordInfo in response [@"resource"]) { 120 | GroupRecord *newRecord=[[GroupRecord alloc]init]; 121 | [newRecord setId:[recordInfo objectForKey:@"id"]]; 122 | [newRecord setName:[recordInfo objectForKey:@"name"]]; 123 | [self.addressBookContentArray addObject:newRecord]; 124 | } 125 | 126 | dispatch_async(dispatch_get_main_queue(),^ (void){ 127 | [self.addressBookTableView reloadData]; 128 | [self.addressBookTableView setNeedsDisplay]; 129 | }); 130 | } failure:^(NSError *error) { 131 | NSLog(@"Error getting address book data: %@",error); 132 | dispatch_async(dispatch_get_main_queue(),^ (void){ 133 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 134 | [message show]; 135 | [self.navigationController popToRootViewControllerAnimated:YES]; 136 | }); 137 | }]; 138 | } 139 | 140 | - (void) removeGroupFromServer:(NSNumber*) groupId{ 141 | 142 | [[RESTEngine sharedEngine] removeGroupFromServerWithGroupId:groupId success:nil failure:^(NSError *error) { 143 | NSLog(@"Error deleting group: %@",error); 144 | dispatch_async(dispatch_get_main_queue(),^ (void){ 145 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 146 | [message show]; 147 | [self.navigationController popToRootViewControllerAnimated:YES]; 148 | }); 149 | }]; 150 | } 151 | 152 | - (void) showContactListViewController{ 153 | dispatch_async(dispatch_queue_create("addressBookQueue", NULL), ^{ 154 | // already fetching so just wait until the data gets back 155 | [self.contactListViewController waitToReady]; 156 | dispatch_async(dispatch_get_main_queue(), ^{ 157 | [self.navigationController pushViewController:self.contactListViewController animated:YES]; 158 | 159 | }); 160 | }); 161 | } 162 | 163 | - (void) showGroupAddViewController{ 164 | GroupAddViewController* groupAddViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"GroupAddViewController"]; 165 | // tell the viewController we are creating a new group 166 | groupAddViewController.groupRecord = nil; 167 | [groupAddViewController prefetch]; 168 | dispatch_async(dispatch_queue_create("contactListShowQueue", NULL), ^{ 169 | 170 | [groupAddViewController waitToReady]; 171 | dispatch_async(dispatch_get_main_queue(), ^{ 172 | [self.navigationController pushViewController:groupAddViewController animated:YES]; 173 | 174 | }); 175 | }); 176 | } 177 | 178 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "CustomNavBar.h" 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (strong, nonatomic) UIWindow *window; 7 | @property (strong, nonatomic) CustomNavBar* globalToolBar; 8 | 9 | 10 | @end 11 | 12 | -------------------------------------------------------------------------------- /example-ios/SampleApp/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "CustomNavBar.h" 3 | 4 | @interface AppDelegate () 5 | 6 | @end 7 | 8 | 9 | @implementation AppDelegate 10 | @synthesize globalToolBar = _globalToolBar; 11 | 12 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 13 | // Override point for customization after application launch. 14 | 15 | // Build persistent navigation bar 16 | CustomNavBar * tb = [CustomNavBar new]; 17 | tb.barStyle = UIBarStyleDefault; 18 | [tb sizeToFit]; 19 | tb.frame = CGRectMake(tb.frame.origin.x, tb.frame.origin.y, tb.frame.size.width,67); 20 | 21 | [tb buildLogo]; 22 | [tb buildButtons]; 23 | [tb reloadInputViews]; 24 | 25 | [_window.rootViewController.view addSubview:tb]; 26 | self.globalToolBar = tb; 27 | 28 | return YES; 29 | } 30 | 31 | - (void)applicationWillResignActive:(UIApplication *)application { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 34 | } 35 | 36 | - (void)applicationDidEnterBackground:(UIApplication *)application { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | - (void)applicationWillEnterForeground:(UIApplication *)application { 42 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 43 | } 44 | 45 | - (void)applicationDidBecomeActive:(UIApplication *)application { 46 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 47 | } 48 | 49 | - (void)applicationWillTerminate:(UIApplication *)application { 50 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactDetailRecord.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ContactDetailRecord_h 2 | #define example_ios_ContactDetailRecord_h 3 | 4 | #import 5 | 6 | // contact info model 7 | @interface ContactDetailRecord : NSObject 8 | 9 | @property(nonatomic, retain) NSNumber* Id; 10 | 11 | @property(nonatomic, retain) NSString* Type; 12 | 13 | @property(nonatomic, retain) NSString* Phone; 14 | @property(nonatomic, retain) NSString* Email; 15 | 16 | @property(nonatomic, retain) NSString* State; 17 | @property(nonatomic, retain) NSString* Zipcode; 18 | @property(nonatomic, retain) NSString* Country; 19 | @property(nonatomic, retain) NSString* City; 20 | @property(nonatomic, retain) NSString* Address; 21 | 22 | @property(nonatomic, retain) NSNumber* ContactId; 23 | 24 | @end 25 | #endif 26 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactDetailRecord.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "ContactDetailRecord.h" 3 | 4 | @implementation ContactDetailRecord 5 | 6 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactEditViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ContactEditViewController_h 2 | #define example_ios_ContactEditViewController_h 3 | 4 | #import 5 | #import "ContactRecord.h" 6 | #import "ProfileImagePickerViewController.h" 7 | #import "ContactViewController.h" 8 | 9 | 10 | @interface ContactEditViewController : UIViewController 11 | 12 | @property(weak, nonatomic) ContactViewController* contactViewController; 13 | 14 | @property(weak, nonatomic) IBOutlet UIScrollView* contactEditScrollView; 15 | 16 | // contact being looked at 17 | @property(nonatomic, retain) ContactRecord* contactRecord; 18 | 19 | // set when editing an existing contact 20 | // list of contactinfo records 21 | @property(nonatomic, retain) NSMutableArray* contactDetails; 22 | 23 | // set when creating a new contact 24 | // id of the group the contact is being created in 25 | @property(nonatomic, retain) NSNumber* contactGroupId; 26 | 27 | @end 28 | #endif 29 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactEditViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | #include "ContactEditViewController.h" 3 | #include "ContactDetailRecord.h" 4 | #include "ContactInfoView.h" 5 | #include "ProfileImagePickerViewController.h" 6 | #import "RESTEngine.h" 7 | #import "AppDelegate.h" 8 | #import "PickerSelector.h" 9 | 10 | @interface ContactEditViewController () 11 | 12 | // all the text fields we programmatically create 13 | @property(nonatomic, retain) NSMutableDictionary* textFields; 14 | 15 | // holds all new contact info fields 16 | @property(nonatomic, retain) NSMutableArray* addedContactInfo; 17 | 18 | // for handling a profile image set up for a new user 19 | @property (nonatomic, retain) NSString* imageUrl; 20 | @property (nonatomic, retain) UIImage* profileImage; 21 | @property (nonatomic, weak) ContactInfoView *selectedContactInfoView; 22 | @property (nonatomic, weak) UITextField *activeTextField; 23 | 24 | @end 25 | 26 | 27 | @implementation ContactEditViewController 28 | 29 | - (void)dealloc 30 | { 31 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 32 | } 33 | 34 | // when adding view controller, need to calc how big it has to be ahead of time 35 | // need to create all the records and such first 36 | - (void) viewDidLoad { 37 | [super viewDidLoad]; 38 | 39 | // Build the view programmatically 40 | self.contactEditScrollView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 41 | 42 | self.contactEditScrollView.backgroundColor = [UIColor colorWithRed:250/255.0f green:250/255.0f blue:250/255.0f alpha:1.0f]; 43 | 44 | self.textFields = [[NSMutableDictionary alloc] init]; 45 | self.addedContactInfo = [[NSMutableArray alloc] init]; 46 | 47 | [self buildContactFields]; 48 | 49 | // resize 50 | CGRect contentRect = CGRectZero; 51 | for (UIView *view in self.contactEditScrollView.subviews) { 52 | contentRect = CGRectUnion(contentRect, view.frame); 53 | } 54 | self.contactEditScrollView.contentSize = contentRect.size; 55 | 56 | self.imageUrl = @""; 57 | self.profileImage = nil; 58 | 59 | [self.view reloadInputViews]; 60 | [self.contactEditScrollView reloadInputViews]; 61 | [self registerForKeyboardNotifications]; 62 | } 63 | 64 | - (void) viewWillDisappear:(BOOL)animated{ 65 | [super viewWillDisappear:animated]; 66 | 67 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 68 | CustomNavBar* navBar = bar.globalToolBar; 69 | [navBar.doneButton removeTarget:self action:@selector(hitSaveButton) forControlEvents:UIControlEventTouchDown]; 70 | } 71 | 72 | - (void) viewWillAppear:(BOOL)animated{ 73 | [super viewWillAppear:animated]; 74 | 75 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 76 | CustomNavBar* navBar = bar.globalToolBar; 77 | [navBar showDone]; 78 | [navBar.doneButton addTarget:self action:@selector(hitSaveButton) forControlEvents:UIControlEventTouchDown]; 79 | [navBar enableAllTouch]; 80 | } 81 | 82 | - (void)registerForKeyboardNotifications 83 | { 84 | [[NSNotificationCenter defaultCenter] addObserver:self 85 | selector:@selector(keyboardWasShown:) 86 | name:UIKeyboardDidShowNotification object:nil]; 87 | 88 | [[NSNotificationCenter defaultCenter] addObserver:self 89 | selector:@selector(keyboardWillBeHidden:) 90 | name:UIKeyboardWillHideNotification object:nil]; 91 | 92 | } 93 | 94 | - (void)keyboardWasShown:(NSNotification*)aNotification 95 | { 96 | NSDictionary* info = [aNotification userInfo]; 97 | CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; 98 | 99 | UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.contactEditScrollView.contentInset.top, 0.0, kbSize.height, 0.0); 100 | self.contactEditScrollView.contentInset = contentInsets; 101 | self.contactEditScrollView.scrollIndicatorInsets = contentInsets; 102 | 103 | // If active text field is hidden by keyboard, scroll it so it's visible 104 | CGRect aRect = self.view.frame; 105 | aRect.size.height -= kbSize.height; 106 | if (!CGRectContainsPoint(aRect, self.activeTextField.frame.origin) ) { 107 | [self.contactEditScrollView scrollRectToVisible:self.activeTextField.frame animated:YES]; 108 | } 109 | } 110 | 111 | - (void)keyboardWillBeHidden:(NSNotification*)aNotification 112 | { 113 | UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.contactEditScrollView.contentInset.top, 0.0, 0.0, 0.0); 114 | self.contactEditScrollView.contentInset = contentInsets; 115 | self.contactEditScrollView.scrollIndicatorInsets = contentInsets; 116 | } 117 | 118 | - (void) addItemViewController:(ProfileImagePickerViewController *)controller didFinishEnteringItem:(NSString *)item { 119 | // gets info passed back up from the image picker 120 | self.contactRecord.ImageUrl = item; 121 | } 122 | 123 | - (void) addItemWithoutContactViewController:(ProfileImagePickerViewController *)controller didFinishEnteringItem:(NSString *)item didChooseImageFromPicker:(UIImage*) image { 124 | // gets info passed back up from the image picker 125 | self.imageUrl = item; 126 | self.profileImage = image; 127 | } 128 | 129 | - (void) hitSaveButton{ 130 | // check that a legal contact name was entered 131 | NSNumber* contactTextFieldId = [NSNumber numberWithInt:-1]; 132 | NSString* firstName = [self getTextValue:@"First Name" recordId:contactTextFieldId]; 133 | NSString* lastName = [self getTextValue:@"Last Name" recordId:contactTextFieldId]; 134 | 135 | if([firstName length] == 0 || [lastName length] == 0){ 136 | UIAlertView *message=[[UIAlertView alloc]initWithTitle:@"" message:@"Please enter a first and last name for the contact." delegate:nil cancelButtonTitle:@"ok" otherButtonTitles: nil]; 137 | [message show]; 138 | return; 139 | } 140 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 141 | CustomNavBar* navBar = bar.globalToolBar; 142 | [navBar disableAllTouch]; 143 | 144 | if(self.contactRecord != nil){ 145 | // if we are editing an existing contact 146 | if(![self.imageUrl isEqual: @""] && self.profileImage != nil){ 147 | [self putLocalImageOnServer:self.profileImage]; 148 | } 149 | else { 150 | [self UpdateContactWithServer]; 151 | } 152 | } 153 | else{ 154 | // need to create the contact before creating addresses or adding 155 | // the contact to any groups 156 | [self addContactToServer]; 157 | } 158 | } 159 | 160 | - (void)onContactTypeClick:(ContactInfoView *)view withTypes:(NSArray *)types 161 | { 162 | PickerSelector *picker = [PickerSelector picker]; 163 | picker.pickerData = types; 164 | picker.delegate = self; 165 | [picker showPickerOver:self]; 166 | self.selectedContactInfoView = view; 167 | } 168 | 169 | - (void)pickerSelector:(PickerSelector *)selector selectedValue:(NSString *)value index:(NSInteger)idx 170 | { 171 | self.selectedContactInfoView.contactType = value; 172 | } 173 | 174 | - (BOOL)textFieldShouldReturn:(UITextField *)textField 175 | { 176 | [textField resignFirstResponder]; 177 | return YES; 178 | } 179 | 180 | - (void)textFieldDidBeginEditing:(UITextField *)textField 181 | { 182 | self.activeTextField = textField; 183 | } 184 | 185 | - (void)textFieldDidEndEditing:(UITextField *)textField 186 | { 187 | self.activeTextField = nil; 188 | } 189 | 190 | - (void) putValueInTextfield:(NSString*)value key:(NSString*)key record:(NSNumber*)record { 191 | if([value length] > 0){ 192 | NSMutableDictionary* dict = [self.textFields objectForKey:record]; 193 | ((UITextField*)[dict objectForKey:key]).text = value; 194 | } 195 | } 196 | 197 | -(void) addNewFieldClicked { 198 | // make room for a new view and insert it 199 | int height = 345; // approximate height of a new info field 200 | CGRect translation; 201 | UIButton* button; 202 | 203 | for(UIView* view in [self.contactEditScrollView subviews]){ 204 | if([view isKindOfClass:[UIButton class]]){ 205 | // find the button from the views 206 | if([((UIButton*) view).titleLabel.text isEqualToString:@"add new address"]){ 207 | button = (UIButton*) view; 208 | } 209 | } 210 | 211 | if([view isKindOfClass:[ContactInfoView class]]){ 212 | if(view.frame.size.height > height){ 213 | // find the size of other contact info views 214 | height = view.frame.size.height; 215 | } 216 | } 217 | } 218 | 219 | int yToInsert = button.frame.origin.y; 220 | 221 | translation.origin.y = yToInsert + height + 30; 222 | 223 | [UIView beginAnimations:nil context:nil]; 224 | [UIView setAnimationDuration:0.25f]; 225 | 226 | // move the button down 227 | button.center = CGPointMake(self.contactEditScrollView.frame.size.width / 2.0f, translation.origin.y + (button.frame.size.height * 0.5)); 228 | 229 | // make the view scroll move down too 230 | CGSize contentRect = self.contactEditScrollView.contentSize; 231 | contentRect.height = button.frame.origin.y + button.frame.size.height; 232 | self.contactEditScrollView.contentSize = contentRect; 233 | CGPoint bottomOffset = CGPointMake(0, translation.origin.y + button.frame.size.height - self.contactEditScrollView.frame.size.height); 234 | 235 | [self.contactEditScrollView setContentOffset:bottomOffset animated:YES]; 236 | [UIView commitAnimations]; 237 | 238 | // build new view 239 | ContactInfoView* contactInfoView = [[ContactInfoView alloc] initWithFrame:CGRectMake(0, yToInsert, self.contactEditScrollView.frame.size.width, 0)]; 240 | contactInfoView.delegate = self; 241 | [contactInfoView setTextFieldsDelegate:self]; 242 | ContactDetailRecord* record = [[ContactDetailRecord alloc] init]; 243 | record.ContactId = nil; 244 | 245 | [self.addedContactInfo addObject:record]; 246 | contactInfoView.record = record; 247 | 248 | [contactInfoView setNeedsDisplay]; 249 | [self.contactEditScrollView addSubview:contactInfoView]; 250 | [self.contactEditScrollView reloadInputViews]; 251 | } 252 | 253 | - (void) changeImageClicked { 254 | ProfileImagePickerViewController* profileImagePickerViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ProfileImagePickerViewController"]; 255 | 256 | // tell the contact list what image it is looking at 257 | profileImagePickerViewController.delegate = self; 258 | 259 | profileImagePickerViewController.record = self.contactRecord; 260 | [self.navigationController pushViewController:profileImagePickerViewController animated:YES]; 261 | } 262 | 263 | - (void) buildContactTextFields: (int)recordId title:(NSString*)title names:(NSArray*)names y:(int)y { 264 | 265 | NSMutableDictionary* dict = [[NSMutableDictionary alloc] initWithCapacity:[names count]+2]; 266 | 267 | for(NSString* field in names){ 268 | UITextField* textfield = [[UITextField alloc] initWithFrame:CGRectMake(self.view.frame.size.width * 0.05, y, self.view.frame.size.width * .9, 35)]; 269 | [textfield setPlaceholder:field]; 270 | [textfield setFont:[UIFont fontWithName:@"Helvetica Neue" size: 20.0]]; 271 | textfield.backgroundColor = [UIColor whiteColor]; 272 | 273 | textfield.layer.cornerRadius = 5; 274 | textfield.delegate = self; 275 | 276 | [self.contactEditScrollView addSubview:textfield]; 277 | 278 | [dict setObject:textfield forKey:field]; 279 | 280 | y += 40; 281 | } 282 | [self.textFields setObject:dict forKey:[NSNumber numberWithInt:recordId]]; 283 | } 284 | 285 | // build ui programmatically 286 | - (void) buildContactFields { 287 | 288 | NSNumber* contactTextfieldId = [NSNumber numberWithInt:-1]; // id for the contact fields that we know could never happen 289 | [self buildContactTextFields:(int)[contactTextfieldId integerValue] title:@"Contact Details" names:@[@"First Name", @"Last Name", @"Twitter", @"Skype", @"Notes"] y:30]; 290 | 291 | // populate the contact fields 292 | if(self.contactRecord != nil){ 293 | [self putValueInTextfield:self.contactRecord.FirstName key:@"First Name" record:contactTextfieldId]; 294 | [self putValueInTextfield:self.contactRecord.LastName key:@"Last Name" record:contactTextfieldId]; 295 | [self putValueInTextfield:self.contactRecord.Twitter key:@"Twitter" record:contactTextfieldId]; 296 | [self putValueInTextfield:self.contactRecord.Skype key:@"Skype" record:contactTextfieldId]; 297 | [self putValueInTextfield:self.contactRecord.Notes key:@"Notes" record:contactTextfieldId]; 298 | } 299 | 300 | //UIButton* changeImageButton = [UIButton buttonWithType:UIButtonTypeSystem]; 301 | //int y = CGRectGetMaxY(((UIView*)[self.contactEditScrollView.subviews lastObject]).frame); 302 | //changeImageButton.frame = CGRectMake(0, y + 10, self.view.frame.size.width, 40); 303 | 304 | //changeImageButton.titleLabel.textAlignment = NSTextAlignmentCenter; 305 | //[changeImageButton setTitle:@"change image" forState:UIControlStateNormal]; 306 | //[changeImageButton.titleLabel setFont:[UIFont fontWithName:@"Helvetica Neue" size: 20.0]]; 307 | //[changeImageButton setTitleColor:[UIColor colorWithRed:107/255.0f green:170/255.0f blue:178/255.0f alpha:1] forState:UIControlStateNormal]; 308 | 309 | //[changeImageButton addTarget:self action:@selector(changeImageClicked) forControlEvents:UIControlEventTouchDown]; 310 | 311 | //[changeImageButton setNeedsDisplay]; 312 | 313 | //[self.contactEditScrollView addSubview:changeImageButton]; 314 | 315 | // add all the contact info views 316 | if(self.contactRecord != nil){ 317 | // if we are not creating a new contact 318 | for(ContactDetailRecord* record in self.contactDetails){ 319 | int y = CGRectGetMaxY(((UIView*)[self.contactEditScrollView.subviews lastObject]).frame); 320 | ContactInfoView* contactInfoView = [[ContactInfoView alloc] initWithFrame:CGRectMake(self.view.frame.size.width * 0.00, y, self.contactEditScrollView.frame.size.width, 40)]; 321 | contactInfoView.delegate = self; 322 | [contactInfoView setTextFieldsDelegate:self]; 323 | 324 | contactInfoView.record = record; 325 | [contactInfoView updateFields]; 326 | 327 | [contactInfoView reloadInputViews]; 328 | [contactInfoView setNeedsDisplay]; 329 | 330 | [self.contactEditScrollView addSubview:contactInfoView]; 331 | [self.contactEditScrollView reloadInputViews]; 332 | } 333 | } 334 | 335 | // create button to add a new field 336 | int y = CGRectGetMaxY(((UIView*)[self.contactEditScrollView.subviews lastObject]).frame); 337 | UIButton* addNewFieldButton = [UIButton buttonWithType:UIButtonTypeSystem]; 338 | 339 | addNewFieldButton.frame=CGRectMake(0, y + 10, self.view.frame.size.width, 40); 340 | addNewFieldButton.backgroundColor = [UIColor colorWithRed:107/255.0f green:170/255.0f blue:178/255.0f alpha:1]; 341 | 342 | addNewFieldButton.titleLabel.textAlignment = NSTextAlignmentCenter; 343 | addNewFieldButton.titleLabel.font =[UIFont fontWithName:@"Helvetica Neue" size: 20.0]; 344 | [addNewFieldButton setTitleColor:[UIColor colorWithRed:254/255.0f green:254/255.0f blue:254/255.0f alpha:1] forState:UIControlStateNormal]; 345 | [addNewFieldButton setTitle:@"add new address" forState:UIControlStateNormal]; 346 | 347 | [addNewFieldButton addTarget:self action:@selector(addNewFieldClicked) forControlEvents:UIControlEventTouchDown]; 348 | 349 | [addNewFieldButton reloadInputViews]; 350 | [addNewFieldButton setNeedsDisplay]; 351 | 352 | [self.contactEditScrollView addSubview:addNewFieldButton]; 353 | } 354 | 355 | - (void)didReceiveMemoryWarning 356 | { 357 | [super didReceiveMemoryWarning]; 358 | } 359 | 360 | - (NSString*) getTextValue:(NSString*)name recordId:(NSNumber*)recordId { 361 | return ((UITextField*)([[self.textFields objectForKey:recordId] objectForKey:name])).text; 362 | } 363 | 364 | - (void) addContactToServer { 365 | // set up the contact name 366 | NSString* filename = @""; 367 | if([self.imageUrl length] > 0){ 368 | filename = [NSString stringWithFormat:@"%@.jpg", self.imageUrl]; 369 | } 370 | 371 | // id given to object holding contact info 372 | NSNumber* contactTextfieldId = [NSNumber numberWithInt:-1]; 373 | 374 | // build request body 375 | NSDictionary *requestBody = @{@"first_name": [self getTextValue:@"First Name" recordId:contactTextfieldId], 376 | @"last_name":[self getTextValue:@"Last Name" recordId:contactTextfieldId], 377 | @"image_url":filename, 378 | @"notes":[self getTextValue:@"Notes" recordId:contactTextfieldId], 379 | @"twitter":[self getTextValue:@"Twitter" recordId:contactTextfieldId], 380 | @"skype":[self getTextValue:@"Skype" recordId:contactTextfieldId]}; 381 | 382 | // build the contact and fill it so we don't have to reload when we go up a level 383 | self.contactRecord = [[ContactRecord alloc] init]; 384 | 385 | [[RESTEngine sharedEngine] addContactToServerWithDetails:requestBody success:^(NSDictionary *response) { 386 | 387 | for (NSDictionary *recordInfo in response[@"resource"]) { 388 | [self.contactRecord setId:[recordInfo objectForKey:@"id"]]; 389 | } 390 | 391 | if(![self.imageUrl isEqual: @""] && self.profileImage != nil){ 392 | [self createProfileImageFolderOnServer]; 393 | } else { 394 | [self addContactGroupRelationToServer]; 395 | } 396 | 397 | } failure:^(NSError *error) { 398 | NSLog(@"Error adding new contact to server: %@",error); 399 | dispatch_async(dispatch_get_main_queue(),^ (void){ 400 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 401 | [message show]; 402 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 403 | }); 404 | }]; 405 | } 406 | 407 | - (void) addContactGroupRelationToServer { 408 | [[RESTEngine sharedEngine] addContactGroupRelationToServerWithContactId:self.contactRecord.Id groupId:self.contactGroupId success:^(NSDictionary *response) { 409 | 410 | [self addContactInfoToServer]; 411 | 412 | } failure:^(NSError *error) { 413 | NSLog(@"Error adding contact group relation to server from contact edit: %@", error); 414 | dispatch_async(dispatch_get_main_queue(),^ (void){ 415 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 416 | [message show]; 417 | [self.navigationController popToRootViewControllerAnimated:YES]; 418 | }); 419 | }]; 420 | } 421 | 422 | 423 | - (void) addContactInfoToServer{ 424 | // build request body 425 | NSMutableArray* records = [[NSMutableArray alloc] init]; 426 | /* 427 | * Format is: 428 | * { 429 | * "resource":[ 430 | * {...}, 431 | * {...} 432 | * ] 433 | * } 434 | * 435 | */ 436 | 437 | // fill body with contact details 438 | for(UIView* view in [self.contactEditScrollView subviews]){ 439 | if([view isKindOfClass:[ContactInfoView class]]){ 440 | ContactInfoView* contactInfoView = (ContactInfoView*) view; 441 | if(contactInfoView.record.Id == nil || [contactInfoView.record.Id isEqualToNumber:@0]){ 442 | contactInfoView.record.Id = [NSNumber numberWithInt:0]; 443 | contactInfoView.record.ContactId = self.contactRecord.Id; 444 | 445 | __block BOOL shouldBreak = NO; 446 | [contactInfoView validateInfoWithResult:^(BOOL valid, NSString *message) { 447 | shouldBreak = !valid; 448 | if(!valid) { 449 | dispatch_async(dispatch_get_main_queue(),^ (void){ 450 | UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 451 | [alert show]; 452 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 453 | }); 454 | } 455 | }]; 456 | if (shouldBreak) { 457 | return; 458 | } 459 | 460 | [contactInfoView updateRecord]; 461 | [records addObject: [contactInfoView buildToDictionary]]; 462 | [self.contactDetails addObject:contactInfoView.record]; 463 | } 464 | } 465 | } 466 | 467 | // make sure we don't try to put contact info up on the server if we don't have any 468 | // need to check down here because of the way they are set up 469 | if([records count] == 0){ 470 | dispatch_async(dispatch_get_main_queue(), ^ (void){ 471 | [self waitToGoBack]; 472 | }); 473 | return; 474 | } 475 | 476 | [[RESTEngine sharedEngine] addContactInfoToServer:records success:^(NSDictionary *response) { 477 | // head back up only once all the data has been loaded 478 | dispatch_async(dispatch_get_main_queue(), ^ (void){ 479 | [self waitToGoBack]; 480 | }); 481 | } failure:^(NSError *error) { 482 | NSLog(@"Error putting contact details back up on server: %@",error); 483 | dispatch_async(dispatch_get_main_queue(),^ (void){ 484 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 485 | [message show]; 486 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 487 | }); 488 | }]; 489 | } 490 | 491 | - (void) createProfileImageFolderOnServer{ 492 | NSString* fileName = @"UserFile1.jpg"; // default file name 493 | if([self.imageUrl length] > 0){ 494 | fileName = [NSString stringWithFormat:@"%@.jpg", self.imageUrl]; 495 | } 496 | 497 | [[RESTEngine sharedEngine] addContactImageWithContactId:self.contactRecord.Id image:self.profileImage imageName:fileName success:^(NSDictionary *response) { 498 | 499 | [self addContactGroupRelationToServer]; 500 | 501 | } failure:^(NSError *error) { 502 | NSLog(@"Error creating new profile image folder on server: %@",error); 503 | dispatch_async(dispatch_get_main_queue(),^ (void){ 504 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 505 | [message show]; 506 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 507 | }); 508 | }]; 509 | } 510 | 511 | - (void) putLocalImageOnServer:(UIImage*) image { 512 | NSString* fileName = @"UserFile1.jpg"; // default file name 513 | if([self.imageUrl length] > 0){ 514 | fileName = [NSString stringWithFormat:@"%@.jpg", self.imageUrl]; 515 | } 516 | 517 | [[RESTEngine sharedEngine] putImageToFolderWithPath:self.contactRecord.Id.stringValue image:image fileName:fileName success:^(NSDictionary *response) { 518 | 519 | self.contactRecord.ImageUrl = fileName; 520 | [self UpdateContactWithServer]; 521 | 522 | } failure:^(NSError *error) { 523 | NSLog(@"Error putting profile image on server: %@",error); 524 | dispatch_async(dispatch_get_main_queue(),^ (void){ 525 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 526 | [message show]; 527 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 528 | }); 529 | }]; 530 | } 531 | 532 | - (void) UpdateContactWithServer { 533 | // -1 is just a tag given to the object that holds the contact info 534 | NSNumber* contactTextfieldId = [NSNumber numberWithInt:-1]; 535 | 536 | NSDictionary *requestBody = @{@"first_name": [self getTextValue:@"First Name" recordId:contactTextfieldId], 537 | @"last_name":[self getTextValue:@"Last Name" recordId:contactTextfieldId], 538 | //@"image_url":self.contactRecord.ImageUrl, 539 | @"notes":[self getTextValue:@"Notes" recordId:contactTextfieldId], 540 | @"twitter":[self getTextValue:@"Twitter" recordId:contactTextfieldId], 541 | @"skype":[self getTextValue:@"Skype" recordId:contactTextfieldId]}; 542 | 543 | // update the contact 544 | self.contactRecord.FirstName = [requestBody objectForKey:@"first_name"]; 545 | self.contactRecord.LastName = [requestBody objectForKey:@"last_name"]; 546 | self.contactRecord.Notes = [requestBody objectForKey:@"notes"]; 547 | self.contactRecord.Twitter = [requestBody objectForKey:@"twitter"]; 548 | self.contactRecord.Skype = [requestBody objectForKey:@"skype"]; 549 | 550 | [[RESTEngine sharedEngine] updateContactWithContactId:self.contactRecord.Id contactDetails:requestBody success:^(NSDictionary *response) { 551 | 552 | [self UpdateContactInfoWithServer]; 553 | 554 | } failure:^(NSError *error) { 555 | NSLog(@"Error updating contact info with server: %@",error); 556 | dispatch_async(dispatch_get_main_queue(),^ (void){ 557 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 558 | [message show]; 559 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 560 | }); 561 | }]; 562 | } 563 | 564 | - (void) UpdateContactInfoWithServer{ 565 | // build request body 566 | NSMutableArray* records = [[NSMutableArray alloc] init]; 567 | for(UIView* view in [self.contactEditScrollView subviews]){ 568 | if([view isKindOfClass:[ContactInfoView class]]){ 569 | ContactInfoView* contactInfoView = (ContactInfoView*) view; 570 | if(contactInfoView.record.ContactId != nil){ 571 | 572 | __block BOOL shouldBreak = NO; 573 | [contactInfoView validateInfoWithResult:^(BOOL valid, NSString *message) { 574 | shouldBreak = !valid; 575 | if(!valid) { 576 | dispatch_async(dispatch_get_main_queue(),^ (void){ 577 | UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 578 | [alert show]; 579 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 580 | }); 581 | } 582 | }]; 583 | if (shouldBreak) { 584 | return; 585 | } 586 | 587 | [contactInfoView updateRecord]; 588 | if(![contactInfoView.record.Id isEqualToNumber:@0]) { 589 | [records addObject: [contactInfoView buildToDictionary]]; 590 | } 591 | } 592 | } 593 | } 594 | if([records count] == 0){ 595 | // if we have no records to update, check if we have any records to add 596 | [self addContactInfoToServer]; 597 | return; 598 | } 599 | 600 | [[RESTEngine sharedEngine] updateContactInfo:records success:^(NSDictionary *response) { 601 | [self addContactInfoToServer]; 602 | } failure:^(NSError *error) { 603 | NSLog(@"Error updating contact details on server: %@",error); 604 | dispatch_async(dispatch_get_main_queue(),^ (void){ 605 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 606 | [message show]; 607 | [((AppDelegate *)[[UIApplication sharedApplication] delegate]).globalToolBar enableAllTouch]; 608 | }); 609 | }]; 610 | } 611 | 612 | - (void) waitToGoBack{ 613 | if(self.contactViewController == nil){ 614 | [self.navigationController popViewControllerAnimated:YES]; 615 | return; 616 | } 617 | dispatch_async(dispatch_queue_create("contactListShowQueue", NULL), ^{ 618 | self.contactViewController.didPrecall = YES; 619 | [self.contactViewController prefetch]; 620 | 621 | [self.contactViewController waitToReady]; 622 | 623 | self.contactViewController = nil; 624 | dispatch_async(dispatch_get_main_queue(), ^{ 625 | [self.navigationController popViewControllerAnimated:YES]; 626 | }); 627 | }); 628 | } 629 | 630 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactInfoView.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ContactInfoView_h 2 | #define example_ios_ContactInfoView_h 3 | 4 | #import 5 | #include "ContactDetailRecord.h" 6 | 7 | @class ContactInfoView; 8 | 9 | @protocol ContactInfoDelegate 10 | 11 | - (void)onContactTypeClick:(ContactInfoView *)view withTypes:(NSArray *)types; 12 | 13 | @end 14 | 15 | // subview of contact view 16 | // displays a contactinfo table record 17 | @interface ContactInfoView : UIView 18 | 19 | @property(nonatomic, retain) ContactDetailRecord* record; 20 | @property (nonatomic, weak) id delegate; 21 | @property (nonatomic, copy) NSString *contactType; 22 | 23 | - (void)setTextFieldsDelegate:(id) delegate; 24 | - (void) updateFields; 25 | - (void) updateRecord; 26 | - (NSDictionary*) buildToDictionary; 27 | - (void)validateInfoWithResult:(void (^)(BOOL valid, NSString *message))result; 28 | 29 | @end 30 | #endif 31 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactInfoView.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "ContactInfoView.h" 4 | 5 | @implementation NSString (emailValidation) 6 | 7 | -(BOOL)isValidEmail 8 | { 9 | NSString *emailRegex = @"^.+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2}[A-Za-z]*$"; 10 | NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex]; 11 | return [emailTest evaluateWithObject:self]; 12 | } 13 | 14 | @end 15 | 16 | @interface ContactInfoView () 17 | 18 | @property (nonatomic, retain) NSMutableDictionary* textFields; 19 | @property (nonatomic, strong) NSArray *contactTypes; 20 | 21 | @end 22 | 23 | @implementation ContactInfoView 24 | 25 | - (id) initWithFrame:(CGRect) frame{ 26 | self = [super initWithFrame:frame]; 27 | 28 | self.contactTypes = @[@"work",@"home",@"mobile",@"other"]; 29 | 30 | [self buildContactTextFields:@[@"Type", @"Phone", @"Email", @"Address", @"City", @"State", @"Zip", @"Country"] y:0]; 31 | 32 | // resize 33 | CGRect contentRect = CGRectZero; 34 | for (UIView *view in self.subviews) { 35 | contentRect = CGRectUnion(contentRect, view.frame); 36 | } 37 | 38 | CGRect oldFrame = self.frame; 39 | oldFrame.size.height = contentRect.size.height; 40 | self.frame = oldFrame; 41 | 42 | return self; 43 | } 44 | 45 | - (void) PutFieldIn:(NSString*)value key:(NSString*)key { 46 | if([value length] > 0){ 47 | ((UITextField*)[self.textFields objectForKey:key]).text = value; 48 | } 49 | else{ 50 | ((UITextField*)[self.textFields objectForKey:key]).text = @""; 51 | } 52 | } 53 | 54 | - (void)setTextFieldsDelegate:(id)delegate 55 | { 56 | [self.textFields enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 57 | ((UITextField *)obj).delegate = delegate; 58 | }]; 59 | } 60 | 61 | - (void) updateFields { 62 | [self PutFieldIn:self.record.Type key:@"Type"]; 63 | [self PutFieldIn:self.record.Phone key:@"Phone" ]; 64 | [self PutFieldIn:self.record.Email key:@"Email" ]; 65 | [self PutFieldIn:self.record.Address key:@"Address" ]; 66 | [self PutFieldIn:self.record.City key:@"City" ]; 67 | [self PutFieldIn:self.record.State key:@"State" ]; 68 | [self PutFieldIn:self.record.Zipcode key:@"Zip" ]; 69 | [self PutFieldIn:self.record.Country key:@"Country" ]; 70 | 71 | [self reloadInputViews]; 72 | [self setNeedsDisplay]; 73 | } 74 | 75 | - (NSString*) getTextValue:(NSString*) key { 76 | return ((UITextField*)[self.textFields objectForKey:key]).text; 77 | } 78 | 79 | - (void) updateRecord { 80 | self.record.Type = [self getTextValue:@"Type" ]; 81 | self.record.Phone = [self getTextValue:@"Phone" ]; 82 | self.record.Email = [self getTextValue:@"Email" ]; 83 | self.record.Address = [self getTextValue:@"Address" ]; 84 | self.record.City = [self getTextValue:@"City" ]; 85 | self.record.State = [self getTextValue:@"State" ]; 86 | self.record.Zipcode = [self getTextValue:@"Zip" ]; 87 | self.record.Country = [self getTextValue:@"Country" ]; 88 | } 89 | 90 | // validate email only 91 | // other validations can be added, e.g. phone number, address 92 | - (void)validateInfoWithResult:(void (^)(BOOL, NSString *))result 93 | { 94 | NSString *email = [self getTextValue:@"Email"]; 95 | if(email.length != 0 && ![email isValidEmail]) { 96 | return result(false, @"Not a valid email"); 97 | } 98 | 99 | return result(true, nil); 100 | } 101 | 102 | - (NSDictionary*) buildToDictionary{ 103 | NSMutableDictionary* dict = [[NSMutableDictionary alloc] initWithDictionary:@{ 104 | @"contact_id":self.record.ContactId, 105 | @"info_type":self.record.Type, 106 | @"phone":self.record.Phone, 107 | @"email":self.record.Email, 108 | @"address":self.record.Address, 109 | @"city":self.record.City, 110 | @"state":self.record.State, 111 | @"zip":self.record.Zipcode, 112 | @"country":self.record.Country 113 | }]; 114 | if (self.record.Id.integerValue != 0) { 115 | dict[@"id"] = self.record.Id; 116 | } 117 | 118 | return dict; 119 | } 120 | 121 | - (void) buildContactTextFields:(NSArray*)names y:(int)y { 122 | y += 30; 123 | 124 | self.textFields = [[NSMutableDictionary alloc] init]; 125 | 126 | for(NSString* field in names){ 127 | UITextField* textfield = [[UITextField alloc] initWithFrame:CGRectMake(self.frame.size.width * 0.05, y, self.frame.size.width * .9, 35)]; 128 | [textfield setPlaceholder:field]; 129 | [textfield setFont:[UIFont fontWithName:@"Helvetica Neue" size: 20.0]]; 130 | textfield.backgroundColor = [UIColor whiteColor]; 131 | 132 | textfield.layer.cornerRadius = 5; 133 | 134 | [self addSubview:textfield]; 135 | 136 | [self.textFields setObject:textfield forKey:field]; 137 | 138 | y += 40; 139 | 140 | if([field isEqualToString:@"Type"]) { 141 | textfield.enabled = NO; 142 | UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; 143 | button.frame = textfield.frame; 144 | [button setTitle:@"" forState:UIControlStateNormal]; 145 | button.backgroundColor = [UIColor clearColor]; 146 | [button addTarget:self action:@selector(onContactTypeClick) forControlEvents:UIControlEventTouchDown]; 147 | [self addSubview:button]; 148 | textfield.text = self.contactTypes[0]; 149 | } 150 | } 151 | } 152 | 153 | - (void)onContactTypeClick 154 | { 155 | [self.delegate onContactTypeClick:self withTypes:self.contactTypes]; 156 | } 157 | 158 | - (void)setContactType:(NSString *)contactType 159 | { 160 | _contactType = [contactType copy]; 161 | ((UITextField *)self.textFields[@"Type"]).text = contactType; 162 | } 163 | 164 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactListViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ContactListTableViewController_h 2 | #define example_ios_ContactListTableViewController_h 3 | 4 | #import 5 | #import "AddressBookRecord.h" 6 | 7 | @interface ContactListViewController : UITableViewController 8 | 9 | 10 | @property (weak, nonatomic) IBOutlet UITableView *contactListTableView; 11 | 12 | // which group is being viewed 13 | @property (nonatomic) GroupRecord* groupRecord; 14 | 15 | - (void) prefetch; 16 | 17 | // blocks until the data has been fetched 18 | - (void) waitToReady; 19 | @end 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactListViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "ContactListViewController.h" 4 | #import "ContactEditViewController.h" 5 | #import "AddressBookRecord.h" 6 | #import "ContactDetailRecord.h" 7 | #import "ContactRecord.h" 8 | #import "ContactViewController.h" 9 | #import "GroupAddViewController.h" 10 | #import "RESTEngine.h" 11 | #import "AppDelegate.h" 12 | 13 | @interface ContactListViewController () 14 | 15 | // results to be displayed during search 16 | @property (nonatomic, retain) NSMutableArray *displayContentArray; 17 | 18 | @property(retain, nonatomic) UISearchBar* searchBar; 19 | 20 | // if there is a search going on 21 | @property(nonatomic) BOOL isSearch; 22 | 23 | // contacts broken into groups by first letter of last name 24 | @property(nonatomic, retain) NSMutableDictionary* contactSectionsDictionary; 25 | 26 | // list of letters 27 | // needs to be mutable because we can delete items 28 | @property(nonatomic, retain) NSMutableArray* alphabetArray; 29 | 30 | // for prefetching data 31 | @property(nonatomic, retain) ContactViewController* contactViewController; 32 | @property(nonatomic) BOOL goingToShowContactViewController; 33 | @property(nonatomic) BOOL didPrefetch; 34 | @property(nonatomic, retain) NSCondition* viewLock; 35 | @property(nonatomic) BOOL viewReady; 36 | 37 | @property(nonatomic, retain) dispatch_queue_t queue; 38 | 39 | 40 | @end 41 | 42 | @implementation ContactListViewController 43 | 44 | 45 | - (void) viewDidLoad { 46 | [super viewDidLoad]; 47 | 48 | self.displayContentArray = [[NSMutableArray alloc] init]; 49 | 50 | // build search bar programmatically 51 | self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)]; 52 | self.searchBar.delegate = self; 53 | self.tableView.tableHeaderView = self.searchBar; 54 | self.isSearch = NO; 55 | 56 | self.contactListTableView.allowsMultipleSelectionDuringEditing = NO; 57 | 58 | [self.contactListTableView setBackgroundColor:[UIColor colorWithRed:254/255.0f green:254/255.0f blue:255/255.0f alpha:1.0f]]; 59 | [self.view setBackgroundColor:[UIColor colorWithRed:254/255.0f green:254/255.0f blue:255/255.0f alpha:1.0f]]; 60 | } 61 | 62 | - (void) viewWillDisappear:(BOOL)animated{ 63 | [super viewWillDisappear:animated]; 64 | 65 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 66 | CustomNavBar* navBar = bar.globalToolBar; 67 | [navBar.addButton removeTarget:self action:@selector(hitAddContactButton) forControlEvents:UIControlEventTouchDown]; 68 | [navBar.editButton removeTarget:self action:@selector(hitGroupEditButton) forControlEvents:UIControlEventTouchDown]; 69 | 70 | if(self.goingToShowContactViewController == NO && self.contactViewController){ 71 | [self.contactViewController canclePrefetch]; 72 | self.contactViewController = nil; 73 | } 74 | } 75 | 76 | - (void) viewWillAppear:(BOOL)animated{ 77 | if(!self.didPrefetch){ 78 | dispatch_async(self.queue, ^{ 79 | [self getContactsListFromServerWithRelation]; 80 | }); 81 | } 82 | [super viewWillAppear:animated]; 83 | self.contactViewController = nil; 84 | self.goingToShowContactViewController = NO; 85 | // reload the view 86 | self.isSearch = NO; 87 | self.searchBar.text = @""; 88 | self.didPrefetch = NO; 89 | 90 | 91 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 92 | CustomNavBar* navBar = bar.globalToolBar; 93 | [navBar.addButton addTarget:self action:@selector(hitAddContactButton) forControlEvents:UIControlEventTouchDown]; 94 | [navBar.editButton addTarget:self action:@selector(hitGroupEditButton) forControlEvents:UIControlEventTouchDown]; 95 | [navBar showEditAndAdd]; 96 | [navBar enableAllTouch]; 97 | } 98 | 99 | - (void)didReceiveMemoryWarning 100 | { 101 | [super didReceiveMemoryWarning]; 102 | } 103 | 104 | - (void) prefetch { 105 | if(self.viewLock == nil){ 106 | self.viewLock = [[NSCondition alloc] init]; 107 | } 108 | [self.viewLock lock]; 109 | self.viewReady = NO; 110 | self.didPrefetch = YES; 111 | 112 | if(self.queue == nil){ 113 | self.queue = dispatch_queue_create("contactListQueue", NULL); 114 | } 115 | dispatch_async(self.queue, ^{ 116 | [self getContactsListFromServerWithRelation]; 117 | }); 118 | } 119 | 120 | - (void) waitToReady{ 121 | [self.viewLock lock]; 122 | while(self.viewReady == NO){ 123 | [self.viewLock wait]; 124 | } 125 | 126 | [self.viewLock unlock]; 127 | } 128 | 129 | - (void) hitAddContactButton { 130 | [self showContactEditViewController]; 131 | } 132 | 133 | - (void) hitGroupEditButton { 134 | [self showGroupEditViewController]; 135 | } 136 | 137 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 138 | if(self.isSearch){ 139 | NSString* searchText = self.searchBar.text; 140 | if([searchText length] > 0){ 141 | return [[searchText substringToIndex:1] uppercaseString]; 142 | } 143 | } 144 | return [self.alphabetArray objectAtIndex:section]; 145 | } 146 | 147 | 148 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ 149 | if(self.isSearch){ 150 | return 1; 151 | } 152 | return [self.alphabetArray count]; 153 | } 154 | 155 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 156 | if(self.isSearch){ 157 | return [self.displayContentArray count]; 158 | } 159 | NSString* sectionKey = [self.alphabetArray objectAtIndex:section]; 160 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionKey]; 161 | return [sectionContacts count]; 162 | } 163 | 164 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 165 | return YES; 166 | } 167 | 168 | - (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section 169 | { 170 | UIColor* tintColor = [UIColor colorWithRed:210/255.0f green:225/255.0f blue:239/255.0f alpha:1.0f]; 171 | view.tintColor = tintColor; 172 | } 173 | 174 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 175 | static NSString *cellIdentifier = @"contactListTableViewCell"; 176 | 177 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifier]; 178 | if(cell == nil){ 179 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; 180 | } 181 | 182 | ContactRecord* record = nil; 183 | if(self.isSearch){ 184 | record = [self.displayContentArray objectAtIndex:indexPath.row]; 185 | } 186 | else{ 187 | NSString* sectionLetter = [self.alphabetArray objectAtIndex:indexPath.section]; 188 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionLetter]; 189 | record = [sectionContacts objectAtIndex:indexPath.row]; 190 | } 191 | 192 | [cell setBackgroundColor:[UIColor colorWithRed:254/255.0f green:254/255.0f blue:255/255.0f alpha:1.0f]]; 193 | cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", record.FirstName, record.LastName]; 194 | cell.textLabel.font = [UIFont fontWithName:@"Helvetica Neue" size: 17.0]; 195 | 196 | return cell; 197 | } 198 | 199 | - (void) tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath{ 200 | ContactRecord* record = nil; 201 | 202 | if(self.isSearch){ 203 | record = [self.displayContentArray objectAtIndex:indexPath.row]; 204 | } 205 | else{ 206 | NSString* sectionLetter = [self.alphabetArray objectAtIndex:indexPath.section]; 207 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionLetter]; 208 | record = [sectionContacts objectAtIndex:indexPath.row]; 209 | } 210 | if(self.contactViewController){ 211 | [self.contactViewController canclePrefetch]; 212 | } 213 | self.contactViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ContactViewController"]; 214 | self.contactViewController.contactRecord = record; 215 | 216 | 217 | [self.contactViewController prefetch]; 218 | self.contactViewController.didPrecall = YES; 219 | } 220 | 221 | -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 222 | ContactRecord* record = nil; 223 | 224 | if(self.isSearch){ 225 | record = [self.displayContentArray objectAtIndex:indexPath.row]; 226 | } 227 | else{ 228 | NSString* sectionLetter = [self.alphabetArray objectAtIndex:indexPath.section]; 229 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionLetter]; 230 | record = [sectionContacts objectAtIndex:indexPath.row]; 231 | } 232 | 233 | // disable touch 234 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 235 | CustomNavBar* navBar = bar.globalToolBar; 236 | [navBar disableAllTouch]; 237 | 238 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 239 | 240 | [self showContactViewController:record]; 241 | } 242 | 243 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 244 | if (editingStyle == UITableViewCellEditingStyleDelete) { 245 | if(self.isSearch){ 246 | ContactRecord* record = [self.displayContentArray objectAtIndex:indexPath.row]; 247 | NSString* index = [[record.LastName substringToIndex:1] uppercaseString]; 248 | NSMutableArray* displayArray = [self.contactSectionsDictionary objectForKey:index]; 249 | [displayArray removeObject:record]; 250 | if([displayArray count] == 0){ 251 | // remove tile header if there are no more tiles in that group 252 | [self.alphabetArray removeObject:index]; 253 | } 254 | // need to delete everything with references to contact before 255 | // removing contact its self 256 | [self removeContactWithContactId:record.Id]; 257 | 258 | [self.displayContentArray removeObjectAtIndex:indexPath.row]; 259 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 260 | } 261 | else{ 262 | NSString* sectionLetter = [self.alphabetArray objectAtIndex:indexPath.section]; 263 | NSMutableArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionLetter]; 264 | ContactRecord* record = [sectionContacts objectAtIndex:indexPath.row]; 265 | [sectionContacts removeObjectAtIndex:indexPath.row]; 266 | 267 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 268 | if( [sectionContacts count] == 0){ 269 | [self.alphabetArray removeObjectAtIndex:indexPath.section]; 270 | } 271 | 272 | // need to delete everything with references to contact before we can 273 | // delete the contact 274 | // delete contact relation -> delete contact info -> delete profile images -> 275 | // delete contact 276 | [self removeContactWithContactId:record.Id]; 277 | } 278 | } 279 | } 280 | 281 | - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{ 282 | [self.displayContentArray removeAllObjects]; 283 | 284 | if([searchText length] == 0){ 285 | self.isSearch = NO; 286 | [self.tableView reloadData]; 287 | return; 288 | } 289 | 290 | self.isSearch = YES; 291 | NSString* firstLetter = [[searchText substringToIndex:1] uppercaseString]; 292 | NSArray* arrayAtLetter = [self.contactSectionsDictionary objectForKey:firstLetter]; 293 | for(ContactRecord* record in arrayAtLetter){ 294 | if([record.LastName length] < [searchText length]){ 295 | continue; 296 | } 297 | NSString* lastNameSubstring = [record.LastName substringToIndex:[searchText length]]; 298 | if([lastNameSubstring caseInsensitiveCompare:searchText] == 0){ 299 | [self.displayContentArray addObject:record]; 300 | } 301 | } 302 | 303 | [self.tableView reloadData]; 304 | } 305 | 306 | - (NSString*) removeNull:(id) input{ 307 | if(input == [NSNull null]){ 308 | return @""; 309 | } 310 | return (NSString*) input; 311 | } 312 | 313 | - (void) getContactsListFromServerWithRelation{ 314 | [[RESTEngine sharedEngine] getContactsListFromServerWithRelationWithGroupId:self.groupRecord.Id success:^(NSDictionary *response) { 315 | self.alphabetArray = [[NSMutableArray alloc] init]; 316 | self.contactSectionsDictionary = [[NSMutableDictionary alloc] init]; 317 | 318 | [self.displayContentArray removeAllObjects]; 319 | [self.contactSectionsDictionary removeAllObjects]; 320 | [self.alphabetArray removeAllObjects]; 321 | 322 | // handle repeat contact-group relationships 323 | NSMutableArray* tmpContactIdList = [[NSMutableArray alloc] init]; 324 | 325 | /* 326 | * Structure of reply is: 327 | * { 328 | * record:[ 329 | * { 330 | * , 331 | * contact_by_contact_id:{ 332 | * 333 | * } 334 | * }, 335 | * ... 336 | * ] 337 | * } 338 | */ 339 | for (NSDictionary *relationRecord in response [@"resource"]) { 340 | @autoreleasepool { 341 | NSDictionary* recordInfo = [relationRecord objectForKey:@"contact_by_contact_id"]; 342 | NSNumber* contactId = [recordInfo objectForKey:@"id"]; 343 | if([tmpContactIdList containsObject:contactId]){ 344 | // a different record already related the group-contact pair 345 | continue; 346 | } 347 | [tmpContactIdList addObject:contactId]; 348 | 349 | ContactRecord* newRecord = [[ContactRecord alloc] init]; 350 | [newRecord setFirstName:[self removeNull:[recordInfo objectForKey:@"first_name"]]]; 351 | [newRecord setLastName:[self removeNull:[recordInfo objectForKey:@"last_name"]]]; 352 | [newRecord setId:[recordInfo objectForKey:@"id"]]; 353 | [newRecord setNotes:[self removeNull:[recordInfo objectForKey:@"notes"]]]; 354 | [newRecord setSkype:[self removeNull:[recordInfo objectForKey:@"skype"]]]; 355 | [newRecord setTwitter:[self removeNull:[recordInfo objectForKey:@"twitter"]]]; 356 | [newRecord setImageUrl:[self removeNull:[recordInfo objectForKey:@"image_url"]]]; 357 | 358 | if([newRecord.LastName length] > 0){ 359 | [self.displayContentArray addObject:newRecord]; 360 | BOOL found = NO; 361 | for(NSString* key in [self.contactSectionsDictionary allKeys]){ 362 | @autoreleasepool { 363 | if([key caseInsensitiveCompare:[newRecord.LastName substringToIndex:1] ] == 0 ){ 364 | // contact fits in one of the buckets already in the dictionary 365 | NSMutableArray* section = [self.contactSectionsDictionary objectForKey:key]; 366 | [section addObject:newRecord]; 367 | found = YES; 368 | break; 369 | } 370 | } 371 | } 372 | if(!found){ 373 | // add new key 374 | NSString* key = [[newRecord.LastName substringToIndex:1] uppercaseString]; 375 | self.contactSectionsDictionary[key] = [[NSMutableArray alloc] initWithObjects:newRecord, nil]; 376 | } 377 | } 378 | } 379 | } 380 | 381 | // sort the sections in the dictionary by lastName, firstName 382 | NSMutableDictionary* tmp = [[NSMutableDictionary alloc] init]; 383 | 384 | for(NSString* key in [self.contactSectionsDictionary allKeys]){ 385 | @autoreleasepool { 386 | NSMutableArray* unsorted = [self.contactSectionsDictionary objectForKey:key]; 387 | NSArray* sorted = [unsorted sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { 388 | 389 | ContactRecord* one = (ContactRecord*) obj1; 390 | ContactRecord* two = (ContactRecord*) obj2; 391 | if([[one LastName] isEqual:[two LastName]]){ 392 | NSString* first = [one FirstName]; 393 | NSString* second = [two FirstName]; 394 | return [first compare:second]; 395 | } 396 | NSString* first = [(ContactRecord*)obj1 LastName]; 397 | NSString* second = [(ContactRecord*)obj2 LastName]; 398 | return [first compare:second]; 399 | }]; 400 | tmp[key] = [sorted mutableCopy]; 401 | } 402 | } 403 | 404 | self.contactSectionsDictionary = tmp; 405 | 406 | self.alphabetArray = [[[self.contactSectionsDictionary allKeys] sortedArrayUsingSelector:@selector(compare:)] mutableCopy]; 407 | 408 | dispatch_async(dispatch_get_main_queue(),^ (void){ 409 | if(!self.viewReady){ 410 | self.viewReady = YES; 411 | [self.viewLock signal]; 412 | [self.viewLock unlock]; 413 | } 414 | else{ 415 | [self.contactListTableView reloadData]; 416 | [self.contactListTableView setNeedsDisplay]; 417 | } 418 | }); 419 | } failure:^(NSError *error) { 420 | if(error.code == 400){ 421 | NSDictionary* decode = [[error.userInfo objectForKey:@"error"] firstObject]; 422 | NSString* message = [decode objectForKey:@"message"]; 423 | if([message containsString:@"Invalid relationship"]){ 424 | NSLog(@"Error: table names in relational calls are case sensitive: %@", message); 425 | dispatch_async(dispatch_get_main_queue(),^ (void){ 426 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 427 | [message show]; 428 | [self.navigationController 429 | popToRootViewControllerAnimated:YES]; 430 | }); 431 | return; 432 | } 433 | } 434 | 435 | NSLog(@"Error getting contacts with relation: %@",error); 436 | dispatch_async(dispatch_get_main_queue(),^ (void){ 437 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 438 | [message show]; 439 | [self.navigationController popToRootViewControllerAnimated:YES]; 440 | }); 441 | }]; 442 | } 443 | 444 | - (void) removeContactWithContactId:(NSNumber*) contactId{ 445 | // remove the contact from the database 446 | [[RESTEngine sharedEngine] removeContactWithContactId:contactId success:^(NSDictionary *response) { 447 | dispatch_async(dispatch_get_main_queue(),^ (void){ 448 | [self.contactListTableView reloadData]; 449 | [self.contactListTableView setNeedsDisplay]; 450 | }); 451 | } failure:^(NSError *error) { 452 | NSLog(@"Error deleting contact: %@",error); 453 | dispatch_async(dispatch_get_main_queue(),^ (void){ 454 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 455 | [message show]; 456 | }); 457 | }]; 458 | } 459 | 460 | - (void) showContactViewController: (ContactRecord*) record { 461 | self.goingToShowContactViewController = YES; 462 | // give the calls on the other end just a little bit of time 463 | dispatch_async(dispatch_queue_create("contactListShowQueue", NULL), ^{ 464 | [self.contactViewController waitToReady]; 465 | dispatch_async(dispatch_get_main_queue(), ^{ 466 | [self.navigationController pushViewController:self.contactViewController animated:YES]; 467 | 468 | }); 469 | }); 470 | } 471 | 472 | - (void) showContactEditViewController{ 473 | ContactEditViewController* contactEditViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ContactEditViewController"]; 474 | // tell the contact list what group it is looking at 475 | contactEditViewController.contactRecord = nil; 476 | contactEditViewController.contactGroupId = self.groupRecord.Id; 477 | contactEditViewController.contactViewController = nil; 478 | [self.navigationController pushViewController:contactEditViewController animated:YES]; 479 | } 480 | 481 | - (void) showGroupEditViewController{ 482 | // same view controller, but set params to edit 483 | GroupAddViewController* groupAddViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"GroupAddViewController"]; 484 | groupAddViewController.groupRecord = self.groupRecord; 485 | [groupAddViewController prefetch]; 486 | dispatch_async(dispatch_queue_create("contactListShowQueue", NULL), ^{ 487 | 488 | [groupAddViewController waitToReady]; 489 | dispatch_async(dispatch_get_main_queue(), ^{ 490 | [self.navigationController pushViewController:groupAddViewController animated:YES]; 491 | 492 | }); 493 | }); 494 | } 495 | 496 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactRecord.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ContactRecord_h 2 | #define example_ios_ContactRecord_h 3 | 4 | // contact model 5 | @interface ContactRecord : NSObject 6 | 7 | @property(nonatomic, retain) NSNumber* Id; 8 | @property(nonatomic, retain) NSString* FirstName; 9 | @property(nonatomic, retain) NSString* LastName; 10 | @property(nonatomic, retain) NSString* Notes; 11 | @property(nonatomic, retain) NSString* Skype; 12 | @property(nonatomic, retain) NSString* Twitter; 13 | @property(nonatomic, retain) NSString* ImageUrl; 14 | 15 | @end 16 | #endif 17 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactRecord.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import 4 | #import "ContactRecord.h" 5 | 6 | @implementation ContactRecord 7 | 8 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ContactViewController_h 2 | #define example_ios_ContactViewController_h 3 | 4 | #import 5 | #import "ContactRecord.h" 6 | 7 | @interface ContactViewController : UIViewController 8 | 9 | @property(weak, nonatomic) IBOutlet UIScrollView* contactDetailScrollView; 10 | 11 | // the contact being looked at 12 | @property(nonatomic, retain) ContactRecord* contactRecord; 13 | 14 | @property(nonatomic) BOOL didPrecall; 15 | 16 | - (void) canclePrefetch; 17 | 18 | - (void) prefetch; 19 | 20 | - (void) waitToReady; 21 | @end 22 | #endif 23 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ContactViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #include "ContactViewController.h" 4 | #include "ContactDetailRecord.h" 5 | #include "ContactEditViewController.h" 6 | #import "RESTEngine.h" 7 | #import "AppDelegate.h" 8 | 9 | @interface ContactViewController () 10 | 11 | // holds contactinfo records 12 | @property(nonatomic, retain) NSMutableArray* contactDetails; 13 | 14 | @property(nonatomic, retain) NSMutableArray* contactGroups; 15 | 16 | @property(nonatomic, retain) dispatch_queue_t queue; 17 | 18 | @property(nonatomic, retain) NSCondition* groupLock; 19 | @property(nonatomic) BOOL groupReady; 20 | 21 | @property(nonatomic, retain) NSCondition* viewLock; 22 | @property(nonatomic) BOOL viewReady; 23 | 24 | @property(nonatomic, retain) NSCondition* waitLock; 25 | @property(nonatomic) BOOL waitReady; 26 | 27 | @property(nonatomic) BOOL cancled; 28 | @end 29 | 30 | @implementation ContactViewController 31 | 32 | // when adding view controller, need to calc how big it has to be ahead of time 33 | // need to create all the records and such first 34 | - (void) viewDidLoad { 35 | [super viewDidLoad]; 36 | 37 | self.contactDetailScrollView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 38 | self.contactDetailScrollView.backgroundColor = [UIColor colorWithRed:254/255.0f green:254/255.0f blue:254/255.0f alpha:1.0f]; 39 | } 40 | 41 | - (void) viewWillDisappear:(BOOL)animated{ 42 | [super viewWillDisappear:animated]; 43 | 44 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 45 | CustomNavBar* navBar = bar.globalToolBar; 46 | [navBar.editButton removeTarget:self action:@selector(hitEditContactButton) forControlEvents:UIControlEventTouchDown]; 47 | } 48 | 49 | 50 | - (void) viewWillAppear:(BOOL)animated{ 51 | [super viewWillAppear:animated]; 52 | if(self.viewReady == NO){ 53 | // only unlock the view if it is locked 54 | self.viewReady = YES; 55 | [self.viewLock signal]; 56 | [self.viewLock unlock]; 57 | } 58 | 59 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 60 | CustomNavBar* navBar = bar.globalToolBar; 61 | [navBar.editButton addTarget:self action:@selector(hitEditContactButton) forControlEvents:UIControlEventTouchDown]; 62 | [navBar showEdit]; 63 | [navBar enableAllTouch]; 64 | self.didPrecall = NO; 65 | } 66 | 67 | - (void) prefetch { 68 | // since there are a bunch of bigger calls (download and relational) needed for this 69 | // view, run the gets at the same time and do the calls asynchronously 70 | // prefetch -> waitLock lock blocks showVC -> contactInfo call finished + unlocks showVC 71 | // view gets pushed, unlocks viewLock -> first page of view gets built as animation comes in 72 | // group list gets added once its done -> picture comes in 73 | // TODO: double check that deadlock can't happen here 74 | 75 | self.contactDetails = [[NSMutableArray alloc] init]; 76 | self.contactGroups = [[NSMutableArray alloc] init]; 77 | 78 | 79 | self.groupLock = [[NSCondition alloc] init]; 80 | self.waitLock = [[NSCondition alloc] init]; 81 | self.viewLock = [[NSCondition alloc] init]; 82 | 83 | self.waitReady = NO; 84 | self.groupReady = NO; 85 | self.viewReady = NO; 86 | self.cancled = NO; 87 | 88 | dispatch_async(dispatch_get_main_queue(), ^{ 89 | [self.waitLock lock]; 90 | [self.viewLock lock]; 91 | [self.groupLock lock]; 92 | }); 93 | 94 | self.queue = dispatch_queue_create("contactViewQueue", NULL); 95 | dispatch_async(self.queue, ^ (void){ 96 | [self getContactInfoFromServer:self.contactRecord]; 97 | }); 98 | dispatch_async(self.queue, ^ (void){ 99 | [self getContactsListFromServerWithRelation]; 100 | }); 101 | 102 | dispatch_async(self.queue, ^ (void){ 103 | [self buildContactView]; 104 | }); 105 | } 106 | 107 | - (void) waitToReady { 108 | [self.waitLock lock]; 109 | while(self.waitReady == NO){ 110 | [self.waitLock wait]; 111 | } 112 | [self.waitLock signal]; 113 | [self.waitLock unlock]; 114 | } 115 | 116 | - (void) canclePrefetch{ 117 | self.cancled = YES; 118 | dispatch_async(dispatch_get_main_queue(), ^{ 119 | self.viewReady = YES; 120 | [self.viewLock signal]; 121 | [self.viewLock unlock]; 122 | }); 123 | } 124 | 125 | - (void) hitEditContactButton{ 126 | ContactEditViewController* contactEditViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ContactEditViewController"]; 127 | // tell the contact list what group it is looking at 128 | contactEditViewController.contactRecord = self.contactRecord; 129 | contactEditViewController.contactViewController = self; 130 | contactEditViewController.contactDetails = self.contactDetails; 131 | [self.navigationController pushViewController:contactEditViewController animated:YES]; 132 | } 133 | 134 | - (void)didReceiveMemoryWarning 135 | { 136 | [super didReceiveMemoryWarning]; 137 | } 138 | 139 | // build the address boxes 140 | - (UIView*) buildAddressView:(ContactDetailRecord*)record y:(NSNumber*)y buffer:(NSNumber*)buffer { 141 | 142 | UIView *subview = [[UIView alloc] initWithFrame:CGRectMake(0, [y doubleValue] + [buffer doubleValue], self.view.frame.size.width, 20)]; 143 | 144 | subview.translatesAutoresizingMaskIntoConstraints = NO; 145 | 146 | UILabel* type_label = [[UILabel alloc] initWithFrame:CGRectMake(25, 10, subview.frame.size.width - 25, 30)]; 147 | type_label.text = record.Type; 148 | type_label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 23.0]; 149 | [type_label setTextColor:[UIColor colorWithRed:253/255.0f green:253/255.0f blue:250/255.0f alpha:1.0f]]; 150 | [subview addSubview:type_label]; 151 | 152 | int next_y = 40; // track where the lowest item is 153 | 154 | if([record.Email length] > 0){ 155 | UILabel* email_label = [[UILabel alloc] initWithFrame:CGRectMake(75, 40, subview.frame.size.width - 75, 30)]; 156 | email_label.text = record.Email; 157 | email_label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 20.0]; 158 | [email_label setTextColor:[UIColor colorWithRed:249/255.0f green:249/255.0f blue:249/255.0f alpha:1.0f]]; 159 | 160 | UIImageView* email_image = [[UIImageView alloc] initWithFrame:CGRectMake(50, 45, 20, 20)]; 161 | [email_image setImage:[UIImage imageNamed:@"mail.png"]]; 162 | [email_image setContentMode:UIViewContentModeScaleAspectFit]; 163 | 164 | [subview addSubview:email_label]; 165 | [subview addSubview:email_image]; 166 | next_y += 60; 167 | } 168 | 169 | if([record.Phone length] > 0){ 170 | UILabel* phone_label = [[UILabel alloc] initWithFrame:CGRectMake(75, next_y, subview.frame.size.width - 75, 20)]; 171 | phone_label.text = record.Phone; 172 | phone_label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 20.0]; 173 | [phone_label setTextColor:[UIColor colorWithRed:253/255.0f green:253/255.0f blue:250/255.0f alpha:1.0f]]; 174 | 175 | UIImageView* phone_image = [[UIImageView alloc] initWithFrame:CGRectMake(50, next_y, 20, 20)]; 176 | [phone_image setImage:[UIImage imageNamed:@"phone1.png"]]; 177 | [phone_image setContentMode:UIViewContentModeScaleAspectFit]; 178 | 179 | [subview addSubview:phone_label]; 180 | [subview addSubview:phone_image]; 181 | 182 | next_y += 60; 183 | } 184 | 185 | if( [record.Address length] > 0 && [record.City length] > 0 && [record.State length] > 0 && [record.Zipcode length] > 0){ 186 | 187 | UILabel* address_label = [[UILabel alloc] initWithFrame:CGRectMake(75, next_y, subview.frame.size.width - 75, 20)]; 188 | [address_label setTextColor:[UIColor colorWithRed:250/255.0f green:250/255.0f blue:250/255.0f alpha:1.0f]]; 189 | 190 | if([record.Country length] > 0){ 191 | address_label.text = [NSString stringWithFormat:@"%@\n%@, %@ %@\n%@", record.Address, record.City, record.State, record.Zipcode, record.Country]; 192 | } 193 | else{ 194 | address_label.text = [NSString stringWithFormat:@"%@\n%@, %@ %@", record.Address, record.City, record.State, record.Zipcode]; 195 | } 196 | 197 | address_label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 19.0]; 198 | address_label.numberOfLines = 0; 199 | [address_label sizeToFit]; 200 | 201 | UIImageView* address_image = [[UIImageView alloc] initWithFrame:CGRectMake(50, next_y, 20, 20)]; 202 | [address_image setImage:[UIImage imageNamed:@"home.png"]]; 203 | [address_image setContentMode:UIViewContentModeScaleAspectFit]; 204 | 205 | [subview addSubview:address_label]; 206 | [subview addSubview:address_image]; 207 | 208 | next_y += address_label.frame.size.height; 209 | } 210 | 211 | // resize the subview 212 | CGRect viewFrame = subview.frame; 213 | viewFrame.size.height = next_y + 20; 214 | viewFrame.origin.x = self.view.frame.size.width * 0.06; 215 | viewFrame.size.width = self.view.frame.size.width * 0.88; 216 | subview.frame = viewFrame; 217 | return subview; 218 | } 219 | 220 | - (UIView*) makeListOfGroupsContactBelongsTo { 221 | UIView* subview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 20)]; 222 | 223 | UILabel* type_label = [[UILabel alloc] initWithFrame:CGRectMake(0, 10, subview.frame.size.width - 25, 30)]; 224 | 225 | type_label.text = @"Groups:"; 226 | type_label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 23.0]; 227 | [subview addSubview:type_label]; 228 | 229 | int y = 50; 230 | for(NSString* groupName in self.contactGroups){ 231 | UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(25, y, subview.frame.size.width - 75, 30)]; 232 | label.text = groupName; 233 | label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 20.0]; 234 | y += 30; 235 | 236 | [subview addSubview:label]; 237 | } 238 | 239 | // resize the subview 240 | CGRect viewFrame = subview.frame; 241 | viewFrame.size.height = y + 20; 242 | viewFrame.origin.x = self.view.frame.size.width * 0.06; 243 | viewFrame.size.width = self.view.frame.size.width * 0.88; 244 | subview.frame = viewFrame; 245 | return subview; 246 | } 247 | 248 | - (void) buildContactView { 249 | // clear out the view 250 | dispatch_sync(dispatch_get_main_queue(), ^{ 251 | [self.contactDetailScrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; 252 | }); 253 | 254 | // go get the profile image 255 | 256 | UIImageView* profile_image = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width * .6, self.view.frame.size.width * .5)]; 257 | [self getProfilePictureFromServer:profile_image]; 258 | 259 | // view lock dependent on 260 | [self.viewLock lock]; 261 | while(self.viewReady == NO){ 262 | // so we don't burn the CPU 263 | [NSThread sleepForTimeInterval:0.0001]; 264 | [self.viewLock wait]; 265 | } 266 | 267 | [self.viewLock unlock]; 268 | 269 | if(self.cancled){ 270 | return; 271 | } 272 | 273 | // track the y of the furthest item down in the view 274 | __block int y = 0; 275 | dispatch_async(dispatch_get_main_queue(), ^{ 276 | [profile_image setCenter:CGPointMake(self.view.frame.size.width * 0.5, profile_image.frame.size.height * 0.5 )]; 277 | 278 | y = profile_image.frame.size.height + 5; 279 | 280 | // add the name label 281 | UILabel* name_label = [[UILabel alloc] initWithFrame:CGRectMake(0, y, self.view.frame.size.width, 35)]; 282 | name_label.text = [NSString stringWithFormat:@"%@ %@", self.contactRecord.FirstName, self.contactRecord.LastName]; 283 | name_label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size: 25.0]; 284 | name_label.textAlignment = NSTextAlignmentCenter; 285 | [self.contactDetailScrollView addSubview:name_label]; 286 | y += 40; 287 | 288 | if([self.contactRecord.Twitter length] > 0){ 289 | UILabel* twitter_label = [[UILabel alloc] initWithFrame:CGRectMake(40, y, self.view.frame.size.width - 40, 20)]; 290 | [twitter_label setFont:[UIFont fontWithName:@"Helvetica Neue" size: 17.0]]; 291 | twitter_label.text = self.contactRecord.Twitter; 292 | 293 | UIImageView* twitter_image = [[UIImageView alloc] initWithFrame:CGRectMake(10, y, 20, 20)]; 294 | 295 | 296 | [twitter_image setImage:[UIImage imageNamed:@"twitter2.png"]]; 297 | [twitter_image setContentMode:UIViewContentModeScaleAspectFit]; 298 | [self.contactDetailScrollView addSubview:twitter_label]; 299 | [self.contactDetailScrollView addSubview:twitter_image]; 300 | 301 | y += 30; 302 | } 303 | 304 | if([self.contactRecord.Skype length] > 0){ 305 | UILabel* skype_label = [[UILabel alloc] initWithFrame:CGRectMake(40, y, self.view.frame.size.width - 40, 20)]; 306 | skype_label.text = self.contactRecord.Skype; 307 | [skype_label setFont:[UIFont fontWithName:@"Helvetica Neue" size: 17.0]]; 308 | 309 | UIImageView* skype_image = [[UIImageView alloc] initWithFrame:CGRectMake(10, y, 20, 20)]; 310 | 311 | [skype_image setImage:[UIImage imageNamed:@"skype.png"]]; 312 | [skype_image setContentMode:UIViewContentModeScaleAspectFit]; 313 | [self.contactDetailScrollView addSubview:skype_label]; 314 | [self.contactDetailScrollView addSubview:skype_image]; 315 | y += 30; 316 | } 317 | 318 | // add the notes 319 | if([self.contactRecord.Notes length ] > 0){ 320 | UILabel* note_title_label = [[UILabel alloc] initWithFrame:CGRectMake(10, y, 80, 25)]; 321 | note_title_label.text = @"Notes"; 322 | [note_title_label setFont:[UIFont fontWithName:@"HelveticaNeue-Light" size: 19.0]]; 323 | y += 20; 324 | 325 | UILabel* notes_label = [[UILabel alloc] initWithFrame:CGRectMake(self.view.frame.size.width * .05, y, self.view.frame.size.width * .9, 80)]; 326 | [notes_label setAutoresizesSubviews:YES]; 327 | 328 | [notes_label setFont:[UIFont fontWithName:@"Helvetica Neue" size: 16.0]]; 329 | notes_label.text = self.contactRecord.Notes; 330 | notes_label.numberOfLines = 0; 331 | [notes_label sizeToFit]; 332 | 333 | [self.contactDetailScrollView addSubview:note_title_label]; 334 | [self.contactDetailScrollView addSubview:notes_label]; 335 | 336 | y += notes_label.frame.size.height + 10; 337 | } 338 | }); 339 | // add all the addresses 340 | UIColor* backgroundColor = [UIColor colorWithRed:112/255.0f green:147/255.0f blue:181/255.0f alpha:1.0f]; 341 | 342 | // don't need to have a lock here because the view lock is dependent on this data loading 343 | dispatch_async(dispatch_get_main_queue(), ^{ 344 | for(ContactDetailRecord* record in self.contactDetails){ 345 | UIView* toAdd = [self buildAddressView:record y:[NSNumber numberWithInt:y] buffer:[NSNumber numberWithInt:25]]; 346 | toAdd.backgroundColor = backgroundColor; 347 | y += toAdd.frame.size.height + 25; 348 | [self.contactDetailScrollView addSubview:toAdd]; 349 | } 350 | }); 351 | 352 | dispatch_sync(dispatch_get_main_queue(), ^{ 353 | [self.contactDetailScrollView reloadInputViews]; 354 | }); 355 | 356 | // wait until the group is ready to build group list subviews 357 | [self.groupLock lock]; 358 | while(self.groupReady == NO){ 359 | [self.groupLock wait]; 360 | } 361 | [self.groupLock unlock]; 362 | 363 | dispatch_sync(dispatch_get_main_queue(), ^{ 364 | UIView* toAdd = [self makeListOfGroupsContactBelongsTo]; 365 | CGRect frame = toAdd.frame; 366 | frame.origin.y = y + 20; 367 | toAdd.frame = frame; 368 | [self.contactDetailScrollView addSubview:toAdd]; 369 | }); 370 | 371 | // resize the scroll view content 372 | dispatch_async(dispatch_get_main_queue(), ^{ 373 | CGRect contentRect = CGRectZero; 374 | for (UIView *view in self.contactDetailScrollView.subviews) { 375 | contentRect = CGRectUnion(contentRect, view.frame); 376 | } 377 | self.contactDetailScrollView.contentSize = contentRect.size; 378 | [self.contactDetailScrollView reloadInputViews]; 379 | }); 380 | 381 | } 382 | 383 | - (NSString*) removeNull:(id) input{ 384 | if(input == [NSNull null]){ 385 | return @""; 386 | } 387 | return (NSString*) input; 388 | } 389 | 390 | - (void) getContactInfoFromServer:(ContactRecord*)contact_record { 391 | [[RESTEngine sharedEngine] getContactInfoFromServerWithContactId:contact_record.Id success:^(NSDictionary *response) { 392 | NSMutableArray* array = [[NSMutableArray alloc] init]; 393 | // put the contact ids into an array 394 | 395 | // double check we don't fetch any repeats 396 | NSMutableArray* existingIds = [[NSMutableArray alloc] init]; 397 | for (NSDictionary *recordInfo in response[@"resource"]) { 398 | @autoreleasepool { 399 | NSNumber* recordId = [recordInfo objectForKey:@"id"]; 400 | if([existingIds containsObject:recordId]){ 401 | continue; 402 | } 403 | ContactDetailRecord* new_record = [[ContactDetailRecord alloc] init]; 404 | [new_record setId:[recordInfo objectForKey:@"id"]]; 405 | [new_record setAddress:[self removeNull:[recordInfo objectForKey:@"address"]]]; 406 | [new_record setCity :[self removeNull:[recordInfo objectForKey:@"city"]]]; 407 | [new_record setCountry:[self removeNull:[recordInfo objectForKey:@"country"]]]; 408 | [new_record setEmail:[self removeNull:[recordInfo objectForKey:@"email"]]]; 409 | [new_record setType:[self removeNull:[recordInfo objectForKey:@"info_type"]]]; 410 | [new_record setPhone:[self removeNull:[recordInfo objectForKey:@"phone"]]]; 411 | [new_record setState:[self removeNull:[recordInfo objectForKey:@"state"]]]; 412 | [new_record setZipcode:[self removeNull:[recordInfo objectForKey:@"zip"]]]; 413 | [new_record setContactId:[recordInfo objectForKey:@"contact_id"]]; 414 | [array addObject:new_record]; 415 | } 416 | } 417 | // tell the contact list what group it is looking at 418 | self.contactDetails = array; 419 | 420 | 421 | dispatch_async(dispatch_get_main_queue(),^ (void){ 422 | self.waitReady = YES; 423 | [self.waitLock signal]; 424 | [self.waitLock unlock]; 425 | }); 426 | } failure:^(NSError *error) { 427 | NSLog(@"Error getting contact info: %@",error); 428 | dispatch_async(dispatch_get_main_queue(),^ (void){ 429 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 430 | [message show]; 431 | [self.navigationController popToRootViewControllerAnimated:YES]; 432 | }); 433 | }]; 434 | } 435 | 436 | - (void) getProfilePictureFromServer:(UIImageView*) image_display{ 437 | 438 | if(self.contactRecord.ImageUrl == nil ||[self.contactRecord.ImageUrl isEqual:@""]){ 439 | dispatch_async(dispatch_get_main_queue(), ^{ 440 | [image_display setImage:[UIImage imageNamed:@"default_portrait.png"]]; 441 | [image_display setContentMode:UIViewContentModeScaleAspectFit]; 442 | [self.contactDetailScrollView addSubview:image_display]; 443 | }); 444 | return; 445 | } 446 | 447 | [[RESTEngine sharedEngine] getProfileImageFromServerWithContactId:self.contactRecord.Id fileName:self.contactRecord.ImageUrl success:^(NSDictionary *response) { 448 | 449 | dispatch_async(dispatch_get_main_queue(),^ (void){ 450 | UIImage* image = nil; 451 | 452 | @try { 453 | NSData *fileData = [[NSData alloc] initWithBase64EncodedString:response [@"content"] options:NSDataBase64DecodingIgnoreUnknownCharacters]; 454 | image = [UIImage imageWithData:fileData]; 455 | } 456 | @catch (NSException *exception) { 457 | NSLog(@"\nWARN: Could not load image off of server, loading default\n"); 458 | image = [UIImage imageNamed:@"default_portrait.png"]; 459 | 460 | } 461 | @finally { 462 | if(image == nil || image.CGImage == nil){ 463 | NSLog(@"\nERROR: Could not make a profile image\n"); 464 | } 465 | } 466 | 467 | [image_display setImage:image]; 468 | [image_display setContentMode:UIViewContentModeScaleAspectFit]; 469 | 470 | [self.contactDetailScrollView addSubview:image_display]; 471 | }); 472 | } failure:^(NSError *error) { 473 | NSLog(@"Error getting profile image data from server: %@",error); 474 | dispatch_async(dispatch_get_main_queue(),^ (void){ 475 | NSLog(@"\nWARN: Could not load image off of server, loading default\n"); 476 | UIImage* image= [UIImage imageNamed:@"default_portrait.png"]; 477 | 478 | [image_display setImage:image]; 479 | [image_display setContentMode:UIViewContentModeScaleAspectFit]; 480 | [self.contactDetailScrollView addSubview:image_display]; 481 | }); 482 | }]; 483 | } 484 | 485 | - (void) getContactsListFromServerWithRelation{ 486 | [[RESTEngine sharedEngine] getContactGroupsWithContactId:self.contactRecord.Id success:^(NSDictionary *response) { 487 | [self.contactGroups removeAllObjects]; 488 | 489 | // handle repeat contact-group relationships 490 | NSMutableArray* tmpGroupIdList = [[NSMutableArray alloc] init]; 491 | 492 | /* 493 | * Structure of reply is: 494 | * { 495 | * record:[ 496 | * { 497 | * , 498 | * contact_group_by_contactGroupId:{ 499 | * 500 | * } 501 | * }, 502 | * ... 503 | * ] 504 | * } 505 | */ 506 | for (NSDictionary *relationRecord in response[@"resource"]) { 507 | NSDictionary* recordInfo = [relationRecord objectForKey:@"contact_group_by_contact_group_id"]; 508 | NSNumber* contactId = [recordInfo objectForKey:@"id"]; 509 | if([tmpGroupIdList containsObject:contactId]){ 510 | // a different record already related the group-contact pair 511 | continue; 512 | } 513 | [tmpGroupIdList addObject:contactId]; 514 | NSString* groupName = [recordInfo objectForKey:@"name"]; 515 | [self.contactGroups addObject:groupName]; 516 | } 517 | 518 | dispatch_async(dispatch_get_main_queue(),^ (void){ 519 | self.groupReady = YES; 520 | [self.groupLock signal]; 521 | [self.groupLock unlock]; 522 | }); 523 | } failure:^(NSError *error) { 524 | NSLog(@"Error getting groups with relation: %@",error); 525 | dispatch_async(dispatch_get_main_queue(),^ (void){ 526 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 527 | [message show]; 528 | [self.navigationController popToRootViewControllerAnimated:YES]; 529 | }); 530 | }]; 531 | } 532 | 533 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/CustomNavBar.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_CustomNavBar_h 2 | #define example_ios_CustomNavBar_h 3 | 4 | #import 5 | 6 | @interface CustomNavBar : UIToolbar 7 | 8 | // custom navigation bar so that we can persistently store the dreamfactory 9 | // logo on the header bar 10 | 11 | @property(retain, nonatomic) UIButton* backButton; 12 | @property(retain, nonatomic) UIButton* addButton; 13 | @property(retain, nonatomic) UIButton* editButton; 14 | @property(retain, nonatomic) UIButton* doneButton; 15 | 16 | - (void) buildLogo; 17 | - (void) buildButtons; 18 | 19 | // make pretty transitions 20 | - (void) showEditAndAdd; 21 | - (void) showAdd; 22 | - (void) showEdit; 23 | - (void) showDone; 24 | 25 | - (void) showEditButton:(BOOL)show; 26 | - (void) showAddButton:(BOOL)show; 27 | - (void) showBackButton:(BOOL)show; 28 | - (void) showDoneButton:(BOOL)show; 29 | 30 | // to prevend silly things with people spamming buttons 31 | - (void) disableAllTouch; 32 | - (void) enableAllTouch; 33 | 34 | @end 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /example-ios/SampleApp/CustomNavBar.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "CustomNavBar.h" 3 | 4 | @interface CustomNavBar () 5 | @property(nonatomic) BOOL isShowEdit; 6 | @property(nonatomic) BOOL isShowAdd; 7 | @property(nonatomic) BOOL isShowDone; 8 | @end 9 | 10 | @implementation CustomNavBar 11 | 12 | - (id)initWithFrame:(CGRect)frame 13 | { 14 | self = [super initWithFrame:frame]; 15 | if (self) { 16 | self.isShowAdd = NO; 17 | self.isShowEdit = NO; 18 | self.isShowDone = NO; 19 | [self setBackgroundColor:[UIColor colorWithRed:240/255.0f green:240/255.0f blue:240/255.0f alpha:1]]; 20 | [self setOpaque:YES]; 21 | // need to set a background image for the view to appear 22 | [self setBackgroundImage:[UIImage imageNamed:@"phone1.png"] forToolbarPosition:UIBarPositionTop barMetrics:UIBarMetricsDefault]; 23 | } 24 | return self; 25 | } 26 | 27 | - (void) buildButtons{ 28 | self.backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; 29 | self.backButton.frame = CGRectMake(self.frame.size.width * .1, 20, 50, 20); 30 | [self.backButton setTitleColor:[UIColor colorWithRed:216/255.0f green:122/255.0f blue:39/255.0f alpha:1] forState:UIControlStateNormal]; 31 | [self.backButton.titleLabel setFont:[UIFont fontWithName:@"HelveticaNeue-Regular" size: 17.0]]; 32 | [self.backButton setTitle:@"Back" forState:UIControlStateNormal]; 33 | self.backButton.center = CGPointMake(self.frame.size.width * .1, 40); 34 | 35 | [self addSubview:self.backButton]; 36 | [self showBackButton:NO]; 37 | 38 | 39 | self.addButton = [UIButton buttonWithType:UIButtonTypeSystem]; 40 | self.addButton.frame = CGRectMake(self.frame.size.width * .85, 20, 50, 40); 41 | [self.addButton.titleLabel setFont:[UIFont fontWithName:@"Helvetica Neue" size: 30.0]]; 42 | [self.addButton setTitleColor: [UIColor colorWithRed:107/255.0f green:170/255.0f blue:178/255.0f alpha:1] forState:UIControlStateNormal]; 43 | [self.addButton setTitle:@"+" forState:UIControlStateNormal]; 44 | self.addButton.center = CGPointMake(self.frame.size.width * .82, 38); 45 | 46 | [self addSubview:self.addButton]; 47 | [self showAddButton:NO]; 48 | 49 | 50 | self.editButton = [UIButton buttonWithType:UIButtonTypeSystem]; 51 | self.editButton.frame = CGRectMake(self.frame.size.width * .8, 20, 50, 40); 52 | [self.editButton setTitleColor:[UIColor colorWithRed:241/255.0f green:141/255.0f blue:42/255.0f alpha:1] forState:UIControlStateNormal]; 53 | 54 | [self.editButton.titleLabel setFont:[UIFont fontWithName:@"HelveticaNeue-Regular" size: 17.0] ]; 55 | [self.editButton setTitle:@"Edit" forState:UIControlStateNormal]; 56 | self.editButton.center = CGPointMake(self.frame.size.width * .93, 40); 57 | 58 | [self addSubview:self.editButton]; 59 | [self showEditButton:NO]; 60 | 61 | 62 | self.doneButton = [UIButton buttonWithType:UIButtonTypeSystem]; 63 | self.doneButton.frame = CGRectMake(self.frame.size.width * .8, 20, 50, 40); 64 | [self.doneButton setTitleColor: [UIColor colorWithRed:102/255.0f green:187/255.0f blue:176/255.0f alpha:1] forState:UIControlStateNormal]; 65 | [self.doneButton setTitleColor:[UIColor colorWithRed:241/255.0f green:141/255.0f blue:42/255.0f alpha:1] forState:UIControlStateNormal]; 66 | [self.doneButton.titleLabel setFont:[UIFont fontWithName:@"Helvetica Neue" size: 17.0]]; 67 | [self.doneButton setTitle:@"Done" forState:UIControlStateNormal]; 68 | self.doneButton.center = CGPointMake(self.frame.size.width * .93, 40); 69 | 70 | [self addSubview:self.doneButton]; 71 | [self showDoneButton:NO]; 72 | } 73 | 74 | - (void) buildLogo{ 75 | UIImage* dfLogo = [UIImage imageNamed:@"DreamFactory-logo-horiz-filled.png"]; 76 | // calc new size 77 | // new height = (imagewidth / newWidth) * imageheight 78 | UIImage* resizable = [dfLogo resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0) resizingMode:UIImageResizingModeStretch]; 79 | 80 | UIImageView* logoView = [[UIImageView alloc] initWithImage:resizable]; 81 | 82 | logoView.frame = CGRectMake(0, 0, self.frame.size.width * .45, dfLogo.size.height* dfLogo.size.width / (self.frame.size.width * .5)); 83 | 84 | logoView.contentMode = UIViewContentModeScaleAspectFit; 85 | 86 | [logoView setCenter:CGPointMake(self.frame.size.width * 0.48, self.frame.size.height * 0.6)]; 87 | 88 | [self addSubview:logoView]; 89 | } 90 | 91 | - (void) showAddButton:(BOOL)show{ 92 | [self.addButton setHidden:!show]; 93 | } 94 | 95 | - (void) showBackButton:(BOOL)show { 96 | [self.backButton setHidden:!show]; 97 | } 98 | 99 | - (void) showEditButton:(BOOL)show { 100 | [self.editButton setHidden:!show]; 101 | } 102 | 103 | - (void) showDoneButton:(BOOL)show{ 104 | [self.doneButton setHidden:!show]; 105 | } 106 | - (void) showAdd { 107 | [self showAddButton:YES]; 108 | [self showDoneButton:NO]; 109 | [self showEditButton:NO]; 110 | } 111 | 112 | - (void) showEditAndAdd{ 113 | [self showDoneButton:NO]; 114 | [self showEditButton:YES]; 115 | [self showAddButton:YES]; 116 | [self showAddButton:YES]; 117 | } 118 | - (void) showDone { 119 | [self showDoneButton:YES]; 120 | [self showEditButton:NO]; 121 | [self showAddButton:NO]; 122 | } 123 | 124 | - (void) showEdit { 125 | [self showDoneButton:NO]; 126 | [self showEditButton:YES]; 127 | [self showAddButton:NO]; 128 | } 129 | 130 | - (void) disableAllTouch { 131 | [self.backButton setUserInteractionEnabled:NO]; 132 | [self.addButton setUserInteractionEnabled:NO]; 133 | [self.editButton setUserInteractionEnabled:NO]; 134 | [self.doneButton setUserInteractionEnabled:NO]; 135 | } 136 | 137 | - (void) enableAllTouch { 138 | [self.backButton setUserInteractionEnabled:YES]; 139 | [self.addButton setUserInteractionEnabled:YES]; 140 | [self.editButton setUserInteractionEnabled:YES]; 141 | [self.doneButton setUserInteractionEnabled:YES]; 142 | 143 | } 144 | 145 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/GroupAddViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_GroupAddViewController_h 2 | #define example_ios_GroupAddViewController_h 3 | 4 | #import 5 | #import "AddressBookRecord.h" 6 | 7 | // view controller for adding a new group and editing an existing group 8 | @interface GroupAddViewController : UIViewController 9 | 10 | @property (weak, nonatomic) IBOutlet UITableView* groupAddTableView; 11 | @property (weak, nonatomic) IBOutlet UITextField* groupNameTextField; 12 | 13 | // set groupRecord and contacts alreadyInGroup when editing an existing group 14 | // contacts already in the existing group 15 | @property (retain, nonatomic) NSMutableArray* contactsAlreadyInGroupContentsArray; 16 | 17 | // record of the group being edited 18 | @property (retain, nonatomic) GroupRecord* groupRecord; 19 | 20 | - (void) prefetch; 21 | - (void) waitToReady; 22 | @end 23 | #endif 24 | -------------------------------------------------------------------------------- /example-ios/SampleApp/GroupAddViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "GroupAddViewController.h" 3 | 4 | #import "RESTEngine.h" 5 | #import "ContactRecord.h" 6 | #import "AddressBookRecord.h" 7 | 8 | #import "AppDelegate.h" 9 | 10 | @interface GroupAddViewController () 11 | 12 | @property(retain, nonatomic) UISearchBar* searchBar; 13 | 14 | // if there is a search going on 15 | @property(nonatomic) BOOL isSearch; 16 | 17 | // holds contents of a search 18 | @property (nonatomic, retain) NSMutableArray *displayContentArray; 19 | 20 | // array of contacts selected to be in the group 21 | @property(nonatomic, retain) NSMutableArray* selectedRows; 22 | 23 | // contacts broken into groups by first letter of last name 24 | @property(nonatomic, retain) NSMutableDictionary* contactSectionsDictionary; 25 | 26 | // header letters 27 | @property(nonatomic, retain) NSArray* alphabetArray; 28 | 29 | @property(nonatomic, retain) NSCondition* waitLock; 30 | @property(nonatomic) BOOL waitReady; 31 | 32 | @end 33 | 34 | @implementation GroupAddViewController 35 | 36 | - (void) viewDidLoad{ 37 | [super viewDidLoad]; 38 | 39 | self.groupNameTextField.delegate = self; 40 | [self.groupNameTextField setValue:[UIColor colorWithRed:180/255.0 green:180/255.0 blue:180/255.0 alpha:1.0] forKeyPath:@"_placeholderLabel.textColor"]; 41 | 42 | // set up the search bar programmatically 43 | self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 118, self.view.frame.size.width, 44)]; 44 | self.searchBar.delegate = self; 45 | self.isSearch = NO; 46 | [self.view addSubview:self.searchBar]; 47 | [self.searchBar setNeedsDisplay]; 48 | [self.view reloadInputViews]; 49 | 50 | if(self.groupRecord != nil) { 51 | // if we are editing a group, get any existing group members 52 | self.groupNameTextField.text = self.groupRecord.Name; 53 | } 54 | 55 | // remove header from table view 56 | self.automaticallyAdjustsScrollViewInsets = NO; 57 | } 58 | 59 | - (void) viewWillAppear:(BOOL)animated{ 60 | [super viewWillAppear:animated]; 61 | 62 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 63 | CustomNavBar* navBar = bar.globalToolBar; 64 | [navBar showDone]; 65 | [navBar.doneButton addTarget:self action:@selector(saveButtonHit) forControlEvents:UIControlEventTouchDown]; 66 | [navBar enableAllTouch]; 67 | } 68 | 69 | - (void) viewWillDisappear:(BOOL)animated{ 70 | [super viewWillDisappear:animated]; 71 | 72 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 73 | CustomNavBar* navBar = bar.globalToolBar; 74 | [navBar.doneButton removeTarget:self action:@selector(saveButtonHit) forControlEvents:UIControlEventTouchDown]; 75 | [navBar disableAllTouch]; 76 | } 77 | 78 | - (void) prefetch { 79 | self.displayContentArray = [[NSMutableArray alloc] init]; 80 | self.contactsAlreadyInGroupContentsArray = [[NSMutableArray alloc]init]; 81 | self.selectedRows = [[NSMutableArray alloc] init]; 82 | 83 | // for sectioned alphabetized list 84 | self.alphabetArray = [[NSMutableArray alloc] init]; 85 | self.contactSectionsDictionary = [[NSMutableDictionary alloc] init]; 86 | 87 | self.waitLock= [[NSCondition alloc] init]; 88 | 89 | [self.waitLock lock]; 90 | self.waitReady = NO; 91 | 92 | dispatch_async(dispatch_queue_create("contactListQueue", NULL), ^{ 93 | [self getContactListFromServer]; 94 | 95 | if(self.groupRecord != nil) { 96 | // if we are editing a group, get any existing group members 97 | [self getContactGroupRelationListFromServer]; 98 | } 99 | }); 100 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 101 | CustomNavBar* navBar = bar.globalToolBar; 102 | [navBar disableAllTouch]; 103 | 104 | } 105 | 106 | - (void) waitToReady { 107 | dispatch_sync(dispatch_queue_create("groupAddWaitQueue", NULL), ^{ 108 | [self.waitLock lock]; 109 | 110 | while (self.waitReady == NO) { 111 | [self.waitLock wait]; 112 | } 113 | [self.waitLock unlock]; 114 | [self.waitLock signal]; 115 | }); 116 | } 117 | 118 | - (void) saveButtonHit{ 119 | if([self.groupNameTextField.text length] == 0){ 120 | // This has a retain cycle with itself, not worth effort to fix 121 | UIAlertView *message=[[UIAlertView alloc]initWithTitle:@"" message:@"Please enter a group name." delegate:nil cancelButtonTitle:@"ok" otherButtonTitles: nil]; 122 | [message show]; 123 | return; 124 | } 125 | if(self.groupRecord != nil){ 126 | // if we are editing a group, head to update 127 | [self buildUpdateRequest]; 128 | } 129 | else{ 130 | [self addNewGroupToServer]; 131 | } 132 | } 133 | 134 | - (BOOL)textFieldShouldReturn:(UITextField *)textField 135 | { 136 | [textField resignFirstResponder]; 137 | return YES; 138 | } 139 | 140 | - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{ 141 | [self.displayContentArray removeAllObjects]; 142 | 143 | if([searchText length] == 0){ 144 | // done with searching, show all the data 145 | self.isSearch = NO;; 146 | [self.groupAddTableView reloadData]; 147 | return; 148 | } 149 | 150 | self.isSearch = YES; 151 | NSString* firstLetter = [[searchText substringToIndex:1] uppercaseString]; 152 | NSArray* arrayAtLetter = [self.contactSectionsDictionary objectForKey:firstLetter]; 153 | for(ContactRecord* record in arrayAtLetter){ 154 | if([record.LastName length] < [searchText length]){ 155 | continue; 156 | } 157 | NSString* lastNameSubstring = [record.LastName substringToIndex:[searchText length]]; 158 | if([lastNameSubstring caseInsensitiveCompare:searchText] == 0){ 159 | [self.displayContentArray addObject:record]; 160 | } 161 | } 162 | [self.groupAddTableView reloadData]; 163 | } 164 | 165 | - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar 166 | { 167 | [searchBar resignFirstResponder]; 168 | } 169 | 170 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 171 | if(self.isSearch){ 172 | NSString* searchText = self.searchBar.text; 173 | if([searchText length] > 0){ 174 | return [[searchText substringToIndex:1] uppercaseString]; 175 | } 176 | } 177 | return [self.alphabetArray objectAtIndex:section]; 178 | } 179 | 180 | 181 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ 182 | if(self.isSearch){ 183 | return 1; 184 | } 185 | return [self.alphabetArray count]; 186 | } 187 | 188 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 189 | if(self.isSearch){ 190 | return [self.displayContentArray count]; 191 | } 192 | 193 | NSString* sectionKey = [self.alphabetArray objectAtIndex:section]; 194 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionKey]; 195 | return [sectionContacts count]; 196 | } 197 | 198 | - (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section 199 | { 200 | // color the section headers 201 | view.tintColor =[UIColor colorWithRed:210/255.0f green:225/255.0f blue:239/255.0f alpha:1.0f]; 202 | } 203 | 204 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 205 | 206 | // build cell for given index path 207 | static NSString *cellIdentifier = @"addGroupContactTableViewCell"; 208 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifier]; 209 | 210 | if(cell == nil){ 211 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; 212 | } 213 | 214 | ContactRecord* record = nil; 215 | 216 | if(self.isSearch){ 217 | record = [self.displayContentArray objectAtIndex:indexPath.row]; 218 | } 219 | else{ 220 | NSString* sectionLetter = [self.alphabetArray objectAtIndex:indexPath.section]; 221 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionLetter]; 222 | record = [sectionContacts objectAtIndex:indexPath.row]; 223 | } 224 | 225 | cell.textLabel.text = [NSString stringWithFormat:@"%@ %@",record.FirstName, record.LastName]; 226 | cell.textLabel.font = [UIFont fontWithName:@"Helvetica Neue" size: 17.0]; 227 | 228 | // if the contact is selected to be in the group, mark it 229 | if([self.selectedRows containsObject:(record.Id)]){ 230 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 231 | } 232 | else{ 233 | cell.accessoryType = UITableViewCellAccessoryNone; 234 | } 235 | 236 | [cell setBackgroundColor:[UIColor colorWithRed:254/255.0f green:254/255.0f blue:255/255.0f alpha:1.0f]]; 237 | 238 | return cell; 239 | } 240 | 241 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 242 | 243 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; 244 | 245 | ContactRecord* contact = nil; 246 | if(self.isSearch){ 247 | contact = [self.displayContentArray objectAtIndex:indexPath.row]; 248 | } 249 | else{ 250 | NSString* sectionKey = [self.alphabetArray objectAtIndex:indexPath.section]; 251 | NSArray* sectionContacts = [self.contactSectionsDictionary objectForKey:sectionKey]; 252 | contact = [sectionContacts objectAtIndex:indexPath.row]; 253 | } 254 | 255 | if(cell.accessoryType == UITableViewCellAccessoryNone ){ 256 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 257 | [self.selectedRows addObject:contact.Id]; 258 | } 259 | else{ 260 | cell.accessoryType = UITableViewCellAccessoryNone; 261 | [self.selectedRows removeObject:contact.Id]; 262 | } 263 | 264 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 265 | } 266 | 267 | - (void) buildUpdateRequest { 268 | // if a contact is selected and was already in the group, do nothing 269 | // if a contact is selected and was not in the group, add it to the group 270 | // if a contact is not selected and was in the group, remove it from the group 271 | 272 | // removing an object mid loop messes with for-each loop 273 | NSMutableArray* quoToRemove = [[NSMutableArray alloc] init]; 274 | for(NSNumber* contactId in self.selectedRows){ 275 | for(int i = 0; i < [self.contactsAlreadyInGroupContentsArray count]; i++){ 276 | if(contactId == [self.contactsAlreadyInGroupContentsArray objectAtIndex:i]){ 277 | [quoToRemove addObject:contactId]; 278 | [self.contactsAlreadyInGroupContentsArray removeObjectAtIndex:i]; 279 | break; 280 | } 281 | } 282 | } 283 | for(NSNumber* contactId in quoToRemove){ 284 | [self.selectedRows removeObject:contactId]; 285 | } 286 | 287 | // remove all the contacts that were in the groups and are not now 288 | // remove contacts from group -> add contacts to group 289 | [self UpdateGroupWithServer]; 290 | } 291 | 292 | - (NSString*) removeNull:(id) input{ 293 | if(input == [NSNull null]){ 294 | return @""; 295 | } 296 | return (NSString*) input; 297 | } 298 | 299 | - (void) getContactListFromServer{ 300 | // get all the contacts in the database 301 | 302 | [[RESTEngine sharedEngine] getContactListFromServerWithSuccess:^(NSDictionary *response) { 303 | // put the contact ids into an array 304 | for (NSDictionary *recordInfo in response [@"resource"]) { 305 | ContactRecord* newRecord = [[ContactRecord alloc] init]; 306 | [newRecord setFirstName:[self removeNull:[recordInfo objectForKey:@"first_name"]]]; 307 | [newRecord setLastName:[self removeNull:[recordInfo objectForKey:@"last_name"]]]; 308 | [newRecord setId:[recordInfo objectForKey:@"id"]]; 309 | 310 | if([newRecord.LastName length] > 0){ 311 | BOOL found = NO; 312 | for(NSString* key in [self.contactSectionsDictionary allKeys]){ 313 | // want to group by last name regardless of case 314 | if([key caseInsensitiveCompare:[newRecord.LastName substringToIndex:1] ] == 0 ){ 315 | NSMutableArray* section = [self.contactSectionsDictionary objectForKey:key]; 316 | [section addObject:newRecord]; 317 | found = YES; 318 | break; 319 | } 320 | } 321 | if(!found){ 322 | // contact doesn't fit in any of the other buckets, make a new one 323 | NSString* key = [[newRecord.LastName substringToIndex:1] uppercaseString]; 324 | self.contactSectionsDictionary[key] = [[NSMutableArray alloc] initWithObjects:newRecord, nil]; 325 | } 326 | } 327 | } 328 | 329 | NSMutableDictionary* tmp = [[NSMutableDictionary alloc] init]; 330 | 331 | // sort the sections alphabetically by last name, first name 332 | for(NSString* key in [self.contactSectionsDictionary allKeys]){ 333 | NSMutableArray* unsorted = [self.contactSectionsDictionary objectForKey:key]; 334 | NSArray* sorted = [unsorted sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { 335 | ContactRecord* one = (ContactRecord*) obj1; 336 | ContactRecord* two = (ContactRecord*) obj2; 337 | if([[one LastName] caseInsensitiveCompare:[two LastName]] == 0){ 338 | NSString* first = [one FirstName]; 339 | NSString* second = [two FirstName]; 340 | return [first compare:second]; 341 | } 342 | NSString* first = [(ContactRecord*)obj1 LastName]; 343 | NSString* second = [(ContactRecord*)obj2 LastName]; 344 | return [first compare:second]; 345 | }]; 346 | tmp[key] = [sorted mutableCopy]; 347 | } 348 | self.contactSectionsDictionary = tmp; 349 | 350 | self.alphabetArray = [[self.contactSectionsDictionary allKeys] sortedArrayUsingSelector:@selector(compare:)]; 351 | 352 | dispatch_async(dispatch_get_main_queue(),^ (void){ 353 | self.waitReady = YES; 354 | [self.waitLock unlock]; 355 | [self.waitLock signal]; 356 | 357 | [self.groupAddTableView reloadData]; 358 | [self.groupAddTableView setNeedsDisplay]; 359 | }); 360 | } failure:^(NSError *error) { 361 | NSLog(@"Error getting all the contacts data: %@",error); 362 | dispatch_async(dispatch_get_main_queue(),^ (void){ 363 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 364 | [message show]; 365 | [self.navigationController popToRootViewControllerAnimated:YES]; 366 | }); 367 | }]; 368 | } 369 | 370 | - (void) addNewGroupToServer { 371 | // created a new group, add it to the server 372 | 373 | [[RESTEngine sharedEngine] addGroupToServerWithName:self.groupNameTextField.text contactIds:self.selectedRows success:^(NSDictionary *response) { 374 | // go to previous screen 375 | dispatch_async(dispatch_get_main_queue(),^ (void){ 376 | [self.navigationController popViewControllerAnimated:YES]; 377 | }); 378 | 379 | } failure:^(NSError *error) { 380 | NSLog(@"Error adding group to server: %@",error); 381 | dispatch_async(dispatch_get_main_queue(),^ (void){ 382 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 383 | [message show]; 384 | [self.navigationController popToRootViewControllerAnimated:YES]; 385 | }); 386 | }]; 387 | } 388 | 389 | - (void) UpdateGroupWithServer{ 390 | 391 | [[RESTEngine sharedEngine] updateGroupWithId:self.groupRecord.Id name:self.groupNameTextField.text oldName:self.groupRecord.Name removedContactIds:self.contactsAlreadyInGroupContentsArray addedContactIds:self.selectedRows success:^(NSDictionary *response) { 392 | 393 | self.groupRecord.Name = self.groupNameTextField.text; 394 | dispatch_async(dispatch_get_main_queue(),^ (void){ 395 | [self.navigationController popViewControllerAnimated:YES]; 396 | }); 397 | 398 | } failure:^(NSError *error) { 399 | NSLog(@"Error updating contact info with server: %@",error); 400 | dispatch_async(dispatch_get_main_queue(),^ (void){ 401 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 402 | [message show]; 403 | [self.navigationController popToRootViewControllerAnimated:YES]; 404 | }); 405 | }]; 406 | } 407 | 408 | 409 | - (void) getContactGroupRelationListFromServer { 410 | [[RESTEngine sharedEngine] getContactGroupRelationListFromServerWithGroupId:self.groupRecord.Id success:^(NSDictionary *response) { 411 | [self.contactsAlreadyInGroupContentsArray removeAllObjects]; 412 | for (NSDictionary *recordInfo in response [@"resource"]) { 413 | [self.contactsAlreadyInGroupContentsArray addObject: [recordInfo objectForKey:@"contact_id"]]; 414 | [self.selectedRows addObject:[recordInfo objectForKey:@"contact_id"]]; 415 | } 416 | 417 | dispatch_async(dispatch_get_main_queue(),^ (void){ 418 | [self.groupAddTableView reloadData]; 419 | [self.groupAddTableView setNeedsDisplay]; 420 | }); 421 | } failure:^(NSError *error) { 422 | NSLog(@"Error getting contact group relations list: %@",error); 423 | dispatch_async(dispatch_get_main_queue(),^ (void){ 424 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 425 | [message show]; 426 | [self.navigationController popToRootViewControllerAnimated:YES]; 427 | }); 428 | }]; 429 | } 430 | 431 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "3x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "3x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "57x57", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "57x57", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "iphone", 45 | "size" : "60x60", 46 | "scale" : "3x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "29x29", 51 | "scale" : "1x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "2x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "40x40", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "2x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "50x50", 71 | "scale" : "1x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "50x50", 76 | "scale" : "2x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "72x72", 81 | "scale" : "1x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "72x72", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ipad", 90 | "size" : "76x76", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "idiom" : "ipad", 95 | "size" : "76x76", 96 | "scale" : "2x" 97 | }, 98 | { 99 | "idiom" : "ipad", 100 | "size" : "83.5x83.5", 101 | "scale" : "2x" 102 | } 103 | ], 104 | "info" : { 105 | "version" : 1, 106 | "author" : "xcode" 107 | } 108 | } -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/DreamFactory-logo-horiz-filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/DreamFactory-logo-horiz-filled.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/DreamFactory-logo-horiz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/DreamFactory-logo-horiz.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "8.0", 8 | "subtype" : "736h", 9 | "scale" : "3x" 10 | }, 11 | { 12 | "orientation" : "landscape", 13 | "idiom" : "iphone", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "8.0", 16 | "subtype" : "736h", 17 | "scale" : "3x" 18 | }, 19 | { 20 | "orientation" : "portrait", 21 | "idiom" : "iphone", 22 | "extent" : "full-screen", 23 | "minimum-system-version" : "8.0", 24 | "subtype" : "667h", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "orientation" : "portrait", 29 | "idiom" : "iphone", 30 | "extent" : "full-screen", 31 | "minimum-system-version" : "7.0", 32 | "scale" : "2x" 33 | }, 34 | { 35 | "orientation" : "portrait", 36 | "idiom" : "iphone", 37 | "extent" : "full-screen", 38 | "minimum-system-version" : "7.0", 39 | "subtype" : "retina4", 40 | "scale" : "2x" 41 | }, 42 | { 43 | "orientation" : "portrait", 44 | "idiom" : "ipad", 45 | "extent" : "full-screen", 46 | "minimum-system-version" : "7.0", 47 | "scale" : "1x" 48 | }, 49 | { 50 | "orientation" : "landscape", 51 | "idiom" : "ipad", 52 | "extent" : "full-screen", 53 | "minimum-system-version" : "7.0", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "orientation" : "portrait", 58 | "idiom" : "ipad", 59 | "extent" : "full-screen", 60 | "minimum-system-version" : "7.0", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "orientation" : "landscape", 65 | "idiom" : "ipad", 66 | "extent" : "full-screen", 67 | "minimum-system-version" : "7.0", 68 | "scale" : "2x" 69 | }, 70 | { 71 | "orientation" : "portrait", 72 | "idiom" : "iphone", 73 | "extent" : "full-screen", 74 | "scale" : "1x" 75 | }, 76 | { 77 | "orientation" : "portrait", 78 | "idiom" : "iphone", 79 | "extent" : "full-screen", 80 | "scale" : "2x" 81 | }, 82 | { 83 | "orientation" : "portrait", 84 | "idiom" : "iphone", 85 | "extent" : "full-screen", 86 | "subtype" : "retina4", 87 | "scale" : "2x" 88 | }, 89 | { 90 | "orientation" : "portrait", 91 | "idiom" : "ipad", 92 | "extent" : "to-status-bar", 93 | "scale" : "1x" 94 | }, 95 | { 96 | "orientation" : "portrait", 97 | "idiom" : "ipad", 98 | "extent" : "full-screen", 99 | "scale" : "1x" 100 | }, 101 | { 102 | "orientation" : "landscape", 103 | "idiom" : "ipad", 104 | "extent" : "to-status-bar", 105 | "scale" : "1x" 106 | }, 107 | { 108 | "orientation" : "landscape", 109 | "idiom" : "ipad", 110 | "extent" : "full-screen", 111 | "scale" : "1x" 112 | }, 113 | { 114 | "orientation" : "portrait", 115 | "idiom" : "ipad", 116 | "extent" : "to-status-bar", 117 | "scale" : "2x" 118 | }, 119 | { 120 | "orientation" : "portrait", 121 | "idiom" : "ipad", 122 | "extent" : "full-screen", 123 | "scale" : "2x" 124 | }, 125 | { 126 | "orientation" : "landscape", 127 | "idiom" : "ipad", 128 | "extent" : "to-status-bar", 129 | "scale" : "2x" 130 | }, 131 | { 132 | "orientation" : "landscape", 133 | "idiom" : "ipad", 134 | "extent" : "full-screen", 135 | "scale" : "2x" 136 | } 137 | ], 138 | "info" : { 139 | "version" : 1, 140 | "author" : "xcode" 141 | } 142 | } -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/chat.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/default_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/default_portrait.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/home.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/mail.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/phone1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/phone1.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/skype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/skype.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Images.xcassets/twitter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/Images.xcassets/twitter2.png -------------------------------------------------------------------------------- /example-ios/SampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIcons 10 | 11 | CFBundleIcons~ipad 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.1 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 1 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIMainStoryboardFile 37 | Main 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example-ios/SampleApp/MasterViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_MasterViewController_h 2 | #define example_ios_MasterViewController_h 3 | 4 | #import 5 | 6 | @interface MasterViewController : UIViewController 7 | 8 | @end 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /example-ios/SampleApp/MasterViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "MasterViewController.h" 3 | #import "AddressBookViewController.h" 4 | #import "RegisterViewController.h" 5 | #import "CustomNavBar.h" 6 | #import "AppDelegate.h" 7 | #import "RESTEngine.h" 8 | 9 | @interface MasterViewController () 10 | 11 | @property (weak, nonatomic) IBOutlet UITextField *emailTextField; 12 | @property (weak, nonatomic) IBOutlet UITextField *passwordTextField; 13 | @property (weak, nonatomic) IBOutlet UILabel *versionLabel; 14 | 15 | - (IBAction)RegisterActionEvent:(id)sender; 16 | 17 | - (IBAction)SubmitActionEvent:(id)sender; 18 | 19 | @end 20 | 21 | @implementation MasterViewController 22 | 23 | - (void)viewDidLoad 24 | { 25 | [super viewDidLoad]; 26 | 27 | self.versionLabel.text = [NSString stringWithFormat:@"Version %@", kAppVersion]; 28 | 29 | NSString *userEmail=[[NSUserDefaults standardUserDefaults] valueForKey:kUserEmail]; 30 | NSString *userPassword=[[NSUserDefaults standardUserDefaults] valueForKey:kPassword]; 31 | 32 | [self.emailTextField setValue:[UIColor colorWithRed:180/255.0 green:180/255.0 blue:180/255.0 alpha:1.0] forKeyPath:@"_placeholderLabel.textColor"]; 33 | [self.passwordTextField setValue:[UIColor colorWithRed:180/255.0 green:180/255.0 blue:180/255.0 alpha:1.0] forKeyPath:@"_placeholderLabel.textColor"]; 34 | 35 | if(userEmail.length >0 && userPassword.length > 0){ 36 | [self.emailTextField setText:userEmail]; 37 | [self.passwordTextField setText:userPassword]; 38 | } 39 | 40 | // set up the custom nav bar 41 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 42 | CustomNavBar* navBar = bar.globalToolBar; 43 | [navBar.backButton addTarget:self action:@selector(hitBackButton) forControlEvents:UIControlEventTouchDown]; 44 | } 45 | - (void) hitBackButton{ 46 | [self.navigationController popViewControllerAnimated:YES]; 47 | } 48 | 49 | - (void) viewWillAppear:(BOOL)animated{ 50 | [super viewWillAppear:animated]; 51 | 52 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 53 | CustomNavBar* navBar = bar.globalToolBar; 54 | [navBar showBackButton:NO]; 55 | [navBar showAddButton:NO]; 56 | [navBar showEditButton:NO]; 57 | [navBar showDoneButton:NO]; 58 | [navBar reloadInputViews]; 59 | } 60 | - (void) viewDidAppear:(BOOL)animated { 61 | if (![[RESTEngine sharedEngine] isConfigured]) { 62 | NSString* msg = @"RESTEngine is not configured.\n\nPlease see README.md."; 63 | [[[UIAlertView alloc]initWithTitle:nil message:msg delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil] show]; 64 | } 65 | } 66 | 67 | 68 | - (IBAction)RegisterActionEvent:(id)sender { 69 | [self showRegisterViewController]; 70 | } 71 | 72 | - (IBAction)SubmitActionEvent:(id)sender { 73 | // log in using the generic API 74 | if(self.emailTextField.text.length>0 && self.passwordTextField.text.length>0) { 75 | [self.emailTextField resignFirstResponder]; 76 | [self.passwordTextField resignFirstResponder]; 77 | 78 | [[RESTEngine sharedEngine] loginWithEmail:self.emailTextField.text password:self.passwordTextField.text success:^(NSDictionary *response) { 79 | [RESTEngine sharedEngine].sessionToken = response[@"session_token"]; 80 | 81 | [[NSUserDefaults standardUserDefaults] setValue:self.emailTextField.text forKey:kUserEmail]; 82 | [[NSUserDefaults standardUserDefaults] setValue:self.passwordTextField.text forKey:kPassword]; 83 | [[NSUserDefaults standardUserDefaults] synchronize]; 84 | 85 | dispatch_async(dispatch_get_main_queue(),^ (void){ 86 | [self showAddressBookViewController]; 87 | }); 88 | } failure:^(NSError *error) { 89 | NSLog(@"Error logging in user: %@",error); 90 | dispatch_async(dispatch_get_main_queue(),^ (void){ 91 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 92 | [message show]; 93 | }); 94 | }]; 95 | } else { 96 | UIAlertView *message=[[UIAlertView alloc]initWithTitle:@"" message:@"Enter email and password" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 97 | [message show]; 98 | } 99 | } 100 | 101 | - (void) showAddressBookViewController { 102 | AddressBookViewController* addressBookViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AddressBookViewController"]; 103 | [self.navigationController pushViewController:addressBookViewController animated:YES]; 104 | } 105 | 106 | - (void) showRegisterViewController { 107 | RegisterViewController* registerViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"RegisterViewController"]; 108 | [self.navigationController pushViewController:registerViewController animated:YES]; 109 | } 110 | 111 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/PickerSelector.h: -------------------------------------------------------------------------------- 1 | // 2 | // PickerSelector.h 3 | // SampleApp 4 | // 5 | // Created by Timur Umayev on 1/14/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class PickerSelector; 12 | 13 | @protocol PickerSelectorDelegate 14 | 15 | @optional 16 | 17 | -(void) pickerSelector:(PickerSelector *)selector selectedValue:(NSString *)value index:(NSInteger)idx; 18 | 19 | @end 20 | 21 | @interface PickerSelector : UIViewController 22 | { 23 | UIViewController *parent_; 24 | CGPoint origin_; 25 | } 26 | 27 | @property (strong, nonatomic) IBOutlet UIPickerView *pickerView; 28 | @property (weak, nonatomic) IBOutlet UIBarButtonItem *cancelButton; 29 | @property (weak, nonatomic) IBOutlet UIBarButtonItem *doneButton; 30 | @property (weak, nonatomic) IBOutlet UIToolbar *optionsToolBar; 31 | 32 | @property (nonatomic, strong) UIView *background; 33 | @property (nonatomic, strong) NSArray *pickerData; 34 | @property (nonatomic, weak) id delegate; 35 | 36 | + (instancetype) picker; 37 | - (void) showPickerOver:(UIViewController *)parent; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /example-ios/SampleApp/PickerSelector.m: -------------------------------------------------------------------------------- 1 | // 2 | // PickerSelector.m 3 | // SampleApp 4 | // 5 | // Created by Timur Umayev on 1/14/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | #import "PickerSelector.h" 10 | 11 | @interface PickerSelector() 12 | 13 | @end 14 | 15 | @implementation PickerSelector 16 | 17 | + (instancetype) picker { 18 | return [PickerSelector pickerWithNibName:@"PickerSelector"]; 19 | } 20 | 21 | + (instancetype) pickerWithNibName:(NSString*)nibName { 22 | PickerSelector *instance = [[self alloc] initWithNibName:nibName bundle:[NSBundle bundleForClass:[PickerSelector class]]]; 23 | instance.pickerData = [NSMutableArray arrayWithCapacity:4]; 24 | 25 | return instance; 26 | } 27 | 28 | - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 29 | { 30 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 31 | if (self) { 32 | CGRect frame = self.view.frame; 33 | 34 | [self.view addSubview:self.pickerView]; 35 | 36 | frame = self.pickerView.frame; 37 | frame.origin.y = CGRectGetMaxY(self.optionsToolBar.frame); 38 | self.pickerView.frame = frame; 39 | } 40 | return self; 41 | } 42 | 43 | - (void) showPickerOver:(UIViewController *)parent 44 | { 45 | UIWindow * window = [[UIApplication sharedApplication] keyWindow]; 46 | 47 | self.background = [[UIView alloc] initWithFrame:window.bounds]; 48 | [self.background setBackgroundColor:[[UIColor blackColor] colorWithAlphaComponent:0.0]]; 49 | [window addSubview:self.background]; 50 | [window addSubview:self.view]; 51 | [parent addChildViewController:self]; 52 | 53 | __block CGRect frame = self.view.frame; 54 | float rotateAngle = 0; 55 | CGSize screenSize = CGSizeMake(window.frame.size.width, [self pickerSize].height); 56 | origin_ = CGPointMake(0,CGRectGetMaxY(window.bounds)); 57 | CGPoint target = CGPointMake(0,origin_.y - CGRectGetHeight(frame)); 58 | 59 | self.view.transform = CGAffineTransformMakeRotation(rotateAngle); 60 | frame = self.view.frame; 61 | frame.size = screenSize; 62 | frame.origin = origin_; 63 | self.view.frame = frame; 64 | 65 | [UIView animateWithDuration:0.3 animations:^{ 66 | self.background.backgroundColor = [self.background.backgroundColor colorWithAlphaComponent:0.5]; 67 | frame = self.view.frame; 68 | frame.origin = target; 69 | self.view.frame = frame; 70 | }]; 71 | 72 | [self.pickerView reloadAllComponents]; 73 | } 74 | 75 | -(CGSize) pickerSize 76 | { 77 | CGSize size = self.view.frame.size; 78 | size.height = CGRectGetHeight(self.optionsToolBar.frame) + CGRectGetHeight(self.pickerView.frame); 79 | size.width = CGRectGetWidth(self.pickerView.frame); 80 | return size; 81 | } 82 | 83 | - (IBAction)setAction:(id)sender 84 | { 85 | if (self.delegate && self.pickerData.count > 0) { 86 | NSInteger index = [self.pickerView selectedRowInComponent:0]; 87 | [self.delegate pickerSelector:self selectedValue:self.pickerData[index] index:index]; 88 | } 89 | 90 | [self dismissPicker]; 91 | } 92 | 93 | - (IBAction)cancelAction:(id)sender 94 | { 95 | [self dismissPicker]; 96 | } 97 | 98 | - (void) dismissPicker 99 | { 100 | [UIView animateWithDuration:0.3 animations:^{ 101 | self.background.backgroundColor = [self.background.backgroundColor colorWithAlphaComponent:0]; 102 | CGRect frame = self.view.frame; 103 | frame.origin = origin_; 104 | self.view.frame = frame; 105 | } completion:^(BOOL finished) { 106 | [self.background removeFromSuperview]; 107 | [self.view removeFromSuperview]; 108 | [self removeFromParentViewController]; 109 | }]; 110 | } 111 | 112 | #pragma mark - picker datasource 113 | 114 | - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView 115 | { 116 | return 1; 117 | } 118 | 119 | - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component 120 | { 121 | return self.pickerData.count; 122 | } 123 | 124 | - (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component 125 | { 126 | return self.pickerData[row]; 127 | } 128 | 129 | @end 130 | -------------------------------------------------------------------------------- /example-ios/SampleApp/PickerSelector.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ProfileImagePickerViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_ProfileImagePickerViewController_h 2 | #define example_ios_ProfileImagePickerViewController_h 3 | 4 | #import 5 | #import "ContactRecord.h" 6 | 7 | @class ProfileImagePickerViewController; 8 | 9 | @protocol ProfileImagePickerViewControllerDelegate 10 | // for sending info back up to the contact-edit controller 11 | - (void)addItemViewController:(ProfileImagePickerViewController *)controller didFinishEnteringItem:(NSString *)item; 12 | 13 | - (void) addItemWithoutContactViewController:(ProfileImagePickerViewController *)controller didFinishEnteringItem:(NSString *)item didChooseImageFromPicker:(UIImage*) image; 14 | @end 15 | 16 | @interface ProfileImagePickerViewController : UIViewController 17 | 18 | @property (retain, nonatomic) IBOutlet UITableView *imageListTableView; 19 | 20 | @property (retain, nonatomic) IBOutlet UITextField *imageNameTextField; 21 | 22 | // set only when editing an existing contact 23 | // the contact we are choosing a profile image for 24 | @property (retain, nonatomic) ContactRecord* record; 25 | 26 | 27 | @property (nonatomic, weak) id delegate; 28 | 29 | @end 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /example-ios/SampleApp/ProfileImagePickerViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "ProfileImagePickerViewController.h" 4 | #import "RESTEngine.h" 5 | #import "AppDelegate.h" 6 | 7 | @interface ProfileImagePickerViewController () 8 | 9 | // holds image picked from the camera roll 10 | @property(nonatomic, retain) UIImage* imageToUpload; 11 | 12 | // list of available profile images 13 | @property (nonatomic, retain) NSMutableArray *imageListContentArray; 14 | 15 | - (IBAction)ChooseLocalActionEvent:(id)sender; 16 | 17 | @end 18 | 19 | @implementation ProfileImagePickerViewController 20 | 21 | @synthesize delegate; 22 | 23 | - (void) viewDidLoad { 24 | [super viewDidLoad]; 25 | 26 | self.imageListContentArray = [[NSMutableArray alloc] init]; 27 | [self getImageListFromServer]; 28 | 29 | [self.imageNameTextField setValue:[UIColor colorWithRed:180/255.0 green:180/255.0 blue:180/255.0 alpha:1.0] forKeyPath:@"_placeholderLabel.textColor"]; 30 | self.imageListTableView.contentInset = UIEdgeInsetsMake(-70, 0, -20, 0); 31 | 32 | self.imageToUpload = nil; 33 | } 34 | 35 | - (void) viewWillDisappear:(BOOL)animated{ 36 | [super viewWillDisappear:animated]; 37 | 38 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 39 | CustomNavBar* navBar = bar.globalToolBar; 40 | [navBar.doneButton removeTarget:self action:@selector(hitSaveButton) forControlEvents:UIControlEventTouchDown]; 41 | } 42 | 43 | - (void) viewWillAppear:(BOOL)animated{ 44 | [super viewWillAppear:animated]; 45 | 46 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 47 | CustomNavBar* navBar = bar.globalToolBar; 48 | [navBar showDone]; 49 | [navBar.doneButton addTarget:self action:@selector(hitSaveButton) forControlEvents:UIControlEventTouchDown]; 50 | } 51 | 52 | - (void) hitSaveButton { 53 | // actually put image up on the server when the contact gets created 54 | if(self.imageToUpload == nil){ 55 | [self.navigationController popViewControllerAnimated:YES]; 56 | } 57 | else{ 58 | // if we chose an image to upload 59 | if([self.imageNameTextField.text length] > 0){ 60 | [self.delegate addItemWithoutContactViewController:self didFinishEnteringItem:self.imageNameTextField.text didChooseImageFromPicker:self.imageToUpload]; 61 | } 62 | else{ 63 | // if we did not name the image 64 | [self.delegate addItemWithoutContactViewController:self didFinishEnteringItem:@"profileImage" didChooseImageFromPicker:self.imageToUpload]; 65 | } 66 | [self.navigationController popViewControllerAnimated:YES]; 67 | } 68 | } 69 | 70 | - (void)didReceiveMemoryWarning 71 | { 72 | [super didReceiveMemoryWarning]; 73 | } 74 | 75 | - (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 76 | return [self.imageListContentArray count]; 77 | } 78 | 79 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 80 | static NSString *cellIdentifier = @"profileImageTableViewCell"; 81 | 82 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifier]; 83 | if(cell == nil){ 84 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; 85 | } 86 | 87 | cell.textLabel.text = [self.imageListContentArray objectAtIndex:indexPath.row]; 88 | 89 | return cell; 90 | } 91 | 92 | -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ 93 | // chose a file off the server 94 | NSString* toPass= [self.imageListContentArray objectAtIndex:indexPath.row]; 95 | [self.delegate addItemViewController:self didFinishEnteringItem:toPass]; 96 | [self.navigationController popViewControllerAnimated:YES]; 97 | } 98 | 99 | - (IBAction) ChooseLocalActionEvent:(id)sender { 100 | // chose an image from the camera roll 101 | UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init]; 102 | imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; 103 | imagePickerController.delegate = self; 104 | [self presentViewController:imagePickerController animated:YES completion:nil]; 105 | } 106 | 107 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info 108 | { 109 | // get image retrieved from the camera roll 110 | UIImage *image = [info valueForKey:UIImagePickerControllerOriginalImage]; 111 | self.imageToUpload = image; 112 | 113 | [picker dismissViewControllerAnimated:YES completion:^{}]; 114 | } 115 | 116 | - (void) getImageListFromServer { 117 | [[RESTEngine sharedEngine] getImageListFromServerWithContactId:self.record.Id success:^(NSDictionary *response) { 118 | [self.imageListContentArray removeAllObjects]; 119 | 120 | for(NSDictionary* fileDict in response[@"file"]){ 121 | [self.imageListContentArray addObject:[fileDict objectForKey:@"name"]]; 122 | } 123 | 124 | dispatch_async(dispatch_get_main_queue(),^ (void){ 125 | [self.imageListTableView reloadData]; 126 | [self.imageListTableView setNeedsDisplay]; 127 | }); 128 | } failure:^(NSError *error) { 129 | // check if the error is file not found 130 | if(error.code == 404){ 131 | NSDictionary* decode = [[error.userInfo objectForKey:@"error"] firstObject]; 132 | NSString* message = [decode objectForKey:@"message"]; 133 | if([message containsString:@"does not exist in storage"]){ 134 | NSLog(@"Warning: Error getting profile image list data from server: %@", message); 135 | return; 136 | } 137 | } 138 | // else report normally 139 | NSLog(@"Error getting profile image list data from server: %@", error); 140 | dispatch_async(dispatch_get_main_queue(),^ (void){ 141 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 142 | [message show]; 143 | [self.navigationController popToRootViewControllerAnimated:YES]; 144 | }); 145 | }]; 146 | } 147 | 148 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/RESTEngine.h: -------------------------------------------------------------------------------- 1 | // 2 | // RESTEngine.h 3 | // SampleApp 4 | // 5 | // Created by Timur Umayev on 1/12/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define kAppVersion @"2.0.4" 12 | 13 | // change kApiKey and kBaseInstanceUrl to match your app and instance 14 | 15 | // Set your service instance and API key here. See: README.md or https://github.com/dreamfactorysoftware/ios-sdk 16 | #define kBaseInstanceUrl @"http://localhost:8080/api/v2" 17 | #define kApiKey @"" 18 | #define kSessionTokenKey @"SessionToken" 19 | #define kDbServiceName @"db/_table" 20 | #define kUserEmail @"UserEmail" 21 | #define kPassword @"UserPassword" 22 | #define kContainerName @"profile_images" 23 | 24 | typedef void (^SuccessBlock)(NSDictionary *response); 25 | typedef void (^FailureBlock)(NSError *error); 26 | 27 | @interface NSError (APIMessage) 28 | 29 | - (NSString *)errorMessage; 30 | 31 | @end 32 | 33 | @implementation NSError (APIMessage) 34 | 35 | - (NSString *)errorMessage 36 | { 37 | NSString *errorMessage = self.userInfo[@"error"][@"message"]; 38 | if(!errorMessage) { 39 | errorMessage = @"Unknown error occurred"; 40 | } 41 | return errorMessage; 42 | } 43 | 44 | @end 45 | 46 | @interface RESTEngine : NSObject 47 | 48 | @property (nonatomic, copy) NSString *sessionToken; 49 | 50 | + (instancetype)sharedEngine; 51 | 52 | - (BOOL) isConfigured; 53 | 54 | /** 55 | Sign in user 56 | */ 57 | - (void)loginWithEmail:(NSString *)email password:(NSString *)password success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 58 | 59 | /** 60 | Register new user 61 | */ 62 | - (void)registerWithEmail:(NSString *)email password:(NSString *)password success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 63 | 64 | /** 65 | Get all the groups from the database 66 | */ 67 | - (void)getAddressBookContentFromServerWithSuccess:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 68 | 69 | /** 70 | Remove group from server 71 | */ 72 | - (void)removeGroupFromServerWithGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 73 | 74 | /** 75 | Add new group with name and contacts 76 | */ 77 | - (void)addGroupToServerWithName:(NSString *)name contactIds:(NSArray *)contactIds success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 78 | 79 | /** 80 | Update group with new name and contacts 81 | */ 82 | - (void)updateGroupWithId:(NSNumber *)groupId name:(NSString *)name oldName:(NSString *)oldName removedContactIds:(NSArray *)removedContactIds addedContactIds:(NSArray *)addedContactIds success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 83 | 84 | /** 85 | Get all the contacts from the database 86 | */ 87 | - (void)getContactListFromServerWithSuccess:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 88 | 89 | /** 90 | Get the list of contacts related to the group 91 | */ 92 | - (void)getContactGroupRelationListFromServerWithGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 93 | 94 | /** 95 | Get all the contacts in the group using relational queries 96 | */ 97 | - (void)getContactsListFromServerWithRelationWithGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 98 | 99 | /** 100 | Remove contact from server 101 | */ 102 | - (void)removeContactWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 103 | 104 | /** 105 | Get contact info from server 106 | */ 107 | - (void)getContactInfoFromServerWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 108 | 109 | /** 110 | Get profile image for contact 111 | */ 112 | - (void)getProfileImageFromServerWithContactId:(NSNumber *)contactId fileName:(NSString *)fileName success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 113 | 114 | /** 115 | Get all the group the contact is in using relational queries 116 | */ 117 | - (void)getContactGroupsWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 118 | 119 | /** 120 | Create contact on server 121 | */ 122 | - (void)addContactToServerWithDetails:(NSDictionary *)contactDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 123 | 124 | 125 | /** 126 | Create contact groupr relations on server 127 | */ 128 | - (void)addContactGroupRelationToServerWithContactId:(NSNumber *)contactId groupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 129 | 130 | 131 | /** 132 | Create contact info on server 133 | */ 134 | - (void)addContactInfoToServer:(NSArray *)info success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 135 | 136 | /** 137 | Create contact image on server 138 | */ 139 | - (void)addContactImageWithContactId:(NSNumber *)contactId image:(UIImage *)image imageName:(NSString *)imageName success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 140 | 141 | - (void)putImageToFolderWithPath:(NSString *)folderPath image:(UIImage *)image fileName:(NSString *)fileName success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 142 | 143 | /** 144 | Update an existing contact with the server 145 | */ 146 | - (void)updateContactWithContactId:(NSNumber *)contactId contactDetails:(NSDictionary *)contactDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 147 | 148 | /** 149 | Update an existing contact info with the server 150 | */ 151 | - (void)updateContactInfo: (NSArray *)info success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 152 | 153 | /** 154 | Get list of contact images from server 155 | */ 156 | - (void)getImageListFromServerWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock; 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /example-ios/SampleApp/RESTEngine.m: -------------------------------------------------------------------------------- 1 | // 2 | // RESTEngine.m 3 | // SampleApp 4 | // 5 | // Created by Timur Umayev on 1/12/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | #import "RESTEngine.h" 10 | #import "NIKApiInvoker.h" 11 | #import "NIKFile.h" 12 | 13 | /** 14 | Routing to different type of API resources 15 | */ 16 | @interface Routing : NSObject 17 | 18 | /** 19 | rest path for request, form is /api/v2/user/resourceName 20 | */ 21 | + (NSString *)userWithResourceName: (NSString *)resourceName; 22 | 23 | /** 24 | rest path for request, form is /api/v2// 25 | */ 26 | + (NSString *)serviceWithTableName: (NSString *)tableName; 27 | 28 | /** 29 | rest path for request, form is /api/v2/files/container// 30 | */ 31 | + (NSString *)resourceFolderWithFolderPath: (NSString *)folderPath; 32 | 33 | /** 34 | rest path for request, form is /api/v2/files/container//filename 35 | */ 36 | + (NSString *)resourceFileWithFolderPath: (NSString *)folderPath fileName:(NSString *)fileName; 37 | 38 | @end 39 | 40 | @implementation Routing 41 | 42 | + (NSString *)userWithResourceName: (NSString *)resourceName 43 | { 44 | return [NSString stringWithFormat:@"%@/user/%@", kBaseInstanceUrl, resourceName]; 45 | } 46 | 47 | + (NSString *)serviceWithTableName: (NSString *)tableName 48 | { 49 | return [NSString stringWithFormat:@"%@/%@/%@", kBaseInstanceUrl, kDbServiceName, tableName]; 50 | } 51 | 52 | + (NSString *)resourceFolderWithFolderPath: (NSString *)folderPath 53 | { 54 | return [NSString stringWithFormat:@"%@/files/%@/%@/", kBaseInstanceUrl, kContainerName, folderPath]; 55 | } 56 | 57 | + (NSString *)resourceFileWithFolderPath: (NSString *)folderPath fileName:(NSString *)fileName 58 | { 59 | return [NSString stringWithFormat:@"%@/files/%@/%@/%@", kBaseInstanceUrl, kContainerName, folderPath, fileName]; 60 | } 61 | 62 | @end 63 | 64 | @interface RESTEngine () 65 | 66 | @property (nonatomic, strong, readonly) NSDictionary *headerParams; 67 | @property (nonatomic, strong, readonly) NSDictionary *sessionHeaderParams; 68 | @property (nonatomic, strong) NIKApiInvoker *api; 69 | 70 | @end 71 | 72 | @implementation RESTEngine 73 | 74 | @synthesize sessionToken = _sessionToken; 75 | @synthesize headerParams = _headerParams; 76 | 77 | + (instancetype)sharedEngine 78 | { 79 | static dispatch_once_t once; 80 | static id sharedInstance = nil; 81 | dispatch_once(&once, ^{ 82 | sharedInstance = [[self alloc] init]; 83 | }); 84 | return sharedInstance; 85 | } 86 | 87 | - (instancetype)init{ 88 | if (self = [super init]) { 89 | _api = [NIKApiInvoker sharedInstance]; 90 | } 91 | return self; 92 | } 93 | - (BOOL) isConfigured { 94 | return ![@"" isEqualToString:kApiKey]; 95 | } 96 | - (NSString *)sessionToken 97 | { 98 | if (_sessionToken == nil) { 99 | _sessionToken = [[NSUserDefaults standardUserDefaults] stringForKey:kSessionTokenKey]; 100 | } 101 | return _sessionToken; 102 | } 103 | 104 | - (void)setSessionToken:(NSString *)sessionToken 105 | { 106 | _sessionToken = sessionToken; 107 | if(sessionToken) { 108 | [[NSUserDefaults standardUserDefaults] setObject:_sessionToken forKey:kSessionTokenKey]; 109 | } else { 110 | [[NSUserDefaults standardUserDefaults] removeObjectForKey:kSessionTokenKey]; 111 | } 112 | [[NSUserDefaults standardUserDefaults] synchronize]; 113 | } 114 | 115 | - (NSDictionary *)headerParams 116 | { 117 | if (_headerParams == nil) { 118 | _headerParams = @{@"X-DreamFactory-Api-Key": kApiKey}; 119 | } 120 | return _headerParams; 121 | } 122 | 123 | - (NSDictionary *)sessionHeaderParams 124 | { 125 | NSMutableDictionary *dict = self.headerParams.mutableCopy; 126 | dict[@"X-DreamFactory-Session-Token"] = self.sessionToken; 127 | return dict; 128 | } 129 | 130 | - (void)callApiWithPath:(NSString *)restApiPath method:(NSString *)method queryParams:(NSDictionary *)queryParams body:(id)body headerParams: (NSDictionary *)headerParams success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock { 131 | [self.api restPath:restApiPath method:method queryParams:queryParams body:body headerParams:headerParams contentType:@"application/json" completionBlock:^(NSDictionary *response, NSError *error) { 132 | if (error !=nil && failureBlock != nil) { 133 | failureBlock(error); 134 | } else if (successBlock != nil) { 135 | successBlock(response); 136 | } 137 | }]; 138 | } 139 | 140 | #pragma mark - Helpers for POST/PUT/PATCH entity wrapping 141 | 142 | - (NSDictionary *) toResourceArray:(NSDictionary *) entity { 143 | // DreamFactory REST API body with {"resource" = [ { record } ] } 144 | NSDictionary *jsonResource = @{@"resource" : @[entity]}; 145 | return jsonResource; 146 | } 147 | - (NSDictionary *) toResourceArrayFromArray:(NSArray *)jsonArray { 148 | NSDictionary *jsonResource = @{@"resource" : jsonArray}; 149 | return jsonResource; 150 | } 151 | 152 | 153 | #pragma mark - Authorization methods 154 | 155 | - (void)loginWithEmail:(NSString *)email password:(NSString *)password success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 156 | { 157 | id requestBody = @{@"email": email, 158 | @"password": password}; 159 | 160 | [self callApiWithPath:[Routing userWithResourceName:@"session"] method:@"POST" queryParams:nil body:requestBody headerParams:self.headerParams success:successBlock failure:failureBlock]; 161 | } 162 | 163 | 164 | - (void)registerWithEmail:(NSString *)email password:(NSString *)password success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 165 | { 166 | //login after signup 167 | NSDictionary *queryParams = @{@"login": @"1"}; 168 | id requestBody = @{@"email": email, 169 | @"password": password, 170 | @"first_name": @"Address", 171 | @"last_name": @"Book", 172 | @"name": @"Address Book User"}; 173 | 174 | [self callApiWithPath:[Routing userWithResourceName:@"register"] method:@"POST" queryParams:queryParams body:requestBody headerParams:self.headerParams success:successBlock failure:failureBlock]; 175 | } 176 | 177 | #pragma mark - Group methods 178 | 179 | - (void)getAddressBookContentFromServerWithSuccess:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 180 | { 181 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group"] method:@"GET" queryParams:nil body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 182 | } 183 | 184 | - (void)removeContactGroupRelationsForGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 185 | { 186 | // remove all contact-group relations for the group being deleted 187 | 188 | // create filter to select all contact_group_relationship records that 189 | // reference the group being deleted 190 | NSDictionary *queryParams = @{@"filter": [NSString stringWithFormat:@"contact_group_id=%@", groupId]}; 191 | 192 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"DELETE" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 193 | } 194 | 195 | - (void)removeGroupFromServerWithGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 196 | { 197 | // can not delete group until all references to it are removed 198 | // remove relations -> remove group 199 | // pass record ID so it knows what group we are removing 200 | 201 | [self removeContactGroupRelationsForGroupId:groupId success:^(NSDictionary *response) { 202 | // delete the record by the record ID 203 | // form is "ids":"1,2,3" 204 | NSDictionary *queryParams = @{@"ids": groupId.stringValue}; 205 | 206 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group"] method:@"DELETE" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 207 | 208 | } failure:failureBlock]; 209 | } 210 | 211 | - (void)addGroupToServerWithName:(NSString *)name contactIds:(NSArray *)contactIds success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 212 | { 213 | id body = [self toResourceArray:@{@"name": name}]; 214 | 215 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group"] method:@"POST" queryParams:nil body:body headerParams:self.sessionHeaderParams success:^(NSDictionary *response) { 216 | // get the id of the new group, then add the relations 217 | NSArray *records = response[@"resource"]; 218 | for(NSDictionary *recordInfo in records) { 219 | [self addGroupContactRelationsForGroupWithId:recordInfo[@"id"] contactIds:contactIds success:successBlock failure:failureBlock]; 220 | } 221 | } failure:failureBlock]; 222 | } 223 | 224 | - (void)addGroupContactRelationsForGroupWithId:(NSNumber *)groupId contactIds:(NSArray *)contactIds success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 225 | { 226 | 227 | // if there are contacts to add skip server update 228 | if (contactIds == nil || contactIds.count == 0) { 229 | successBlock(nil); 230 | return; 231 | } 232 | 233 | // build request body 234 | /* 235 | * structure of request is: 236 | * { 237 | * "resource":[ 238 | * { 239 | * "contact_group_id":id, 240 | * "contact_id":id" 241 | * }, 242 | * {...} 243 | * ] 244 | * } 245 | */ 246 | 247 | NSMutableArray *records = [[NSMutableArray alloc] init]; 248 | for (NSNumber *contactId in contactIds) { 249 | [records addObject:@{@"contact_group_id": groupId, @"contact_id": contactId}]; 250 | } 251 | 252 | id body = @{@"resource": records}; 253 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"POST" queryParams:nil body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 254 | } 255 | 256 | - (void)updateGroupWithId:(NSNumber *)groupId name:(NSString *)name oldName:(NSString *)oldName removedContactIds:(NSArray *)removedContactIds addedContactIds:(NSArray *)addedContactIds success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 257 | { 258 | //if name didn't change skip server update 259 | if([oldName isEqualToString:name]) { 260 | [self removeGroupContactRelationsForGroupWithId:groupId contactIds:removedContactIds success:^(NSDictionary *response) { 261 | 262 | [self addGroupContactRelationsForGroupWithId:groupId contactIds:addedContactIds success:successBlock failure:failureBlock]; 263 | 264 | } failure:failureBlock]; 265 | return; 266 | } 267 | 268 | // update name 269 | NSDictionary *queryParams = @{@"ids": groupId}; 270 | id body = [self toResourceArray:@{@"name": name}]; 271 | 272 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group"] method:@"PATCH" queryParams:queryParams body:body headerParams:self.sessionHeaderParams success:^(NSDictionary *response) { 273 | 274 | [self removeGroupContactRelationsForGroupWithId:groupId contactIds:removedContactIds success:^(NSDictionary *response) { 275 | 276 | [self addGroupContactRelationsForGroupWithId:groupId contactIds:addedContactIds success:successBlock failure:failureBlock]; 277 | 278 | } failure:failureBlock]; 279 | 280 | } failure:failureBlock]; 281 | } 282 | 283 | - (void)removeGroupContactRelationsForGroupWithId:(NSNumber *)groupId contactIds:(NSArray *)contactIds success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 284 | { 285 | // if there are no contacts to remove skip server update 286 | if (contactIds == nil || contactIds.count == 0) { 287 | successBlock(nil); 288 | return; 289 | } 290 | 291 | // remove contact-group relations 292 | 293 | // do not know the ID of the record to remove 294 | // one value for groupId, but many values for contactId 295 | // instead of making a long SQL query, change what we use as identifiers 296 | NSDictionary *queryParams = @{@"id_field": @"contact_group_id,contact_id"}; 297 | 298 | // build request body 299 | /* 300 | * structure of request is: 301 | * { 302 | * "resource":[ 303 | * { 304 | * "contact_group_id":id, 305 | * "contact_id":id" 306 | * }, 307 | * {...} 308 | * ] 309 | * } 310 | */ 311 | NSMutableArray *records = [[NSMutableArray alloc] init]; 312 | for (NSNumber *contactId in contactIds) { 313 | [records addObject:@{@"contact_group_id": groupId, @"contact_id": contactId}]; 314 | } 315 | 316 | id body = @{@"resource": records}; 317 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"DELETE" queryParams:queryParams body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 318 | } 319 | 320 | #pragma mark - Contact methods 321 | 322 | - (void)getContactListFromServerWithSuccess:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 323 | { 324 | // only need to get the contactId and full contact name 325 | // set the fields param to give us just the fields we need 326 | NSDictionary *queryParams = @{@"fields": @"id,first_name,last_name"}; 327 | 328 | [self callApiWithPath:[Routing serviceWithTableName:@"contact"] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 329 | } 330 | 331 | - (void)getContactGroupRelationListFromServerWithGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 332 | { 333 | // create filter to get only the contact in the group 334 | NSDictionary *queryParams = @{@"filter": [NSString stringWithFormat:@"contact_group_id=%@", groupId]}; 335 | 336 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 337 | } 338 | 339 | - (void)getContactsListFromServerWithRelationWithGroupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 340 | { 341 | // only get contact_group_relationships for this group 342 | NSMutableDictionary *queryParams = [@{@"filter": [NSString stringWithFormat:@"contact_group_id=%@", groupId]} mutableCopy]; 343 | 344 | // request without related would return just {id, groupId, contactId} 345 | // set the related field to go get the contact records referenced by 346 | // each contact_group_relationship record 347 | queryParams[@"related"] = @"contact_by_contact_id"; 348 | 349 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 350 | } 351 | 352 | - (void)removeContactWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 353 | { 354 | // need to delete everything with references to contact before we can delete the contact 355 | // delete contact relation -> delete contact info -> delete profile images -> delete contact 356 | // remove contact by record ID 357 | 358 | [self removeContactRelationWithContactId:contactId success:^(NSDictionary *response) { 359 | [self removeContactInfoWithContactId:contactId success:^(NSDictionary *response) { 360 | [self removeContactImageFolderWithContactId:contactId success:^(NSDictionary *response) { 361 | 362 | NSDictionary *queryParams = @{@"ids": contactId.stringValue}; 363 | [self callApiWithPath:[Routing serviceWithTableName:@"contact"] method:@"DELETE" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 364 | 365 | } failure:failureBlock]; 366 | } failure:failureBlock]; 367 | } failure:failureBlock]; 368 | } 369 | 370 | - (void)removeContactRelationWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 371 | { 372 | // remove only contact-group relationships where contact is the contact to remove 373 | NSDictionary *queryParams = @{@"filter": [NSString stringWithFormat:@"contact_id=%@", contactId]}; 374 | 375 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"DELETE" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 376 | } 377 | 378 | - (void)removeContactInfoWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 379 | { 380 | // remove only contact info for the contact we want to remove 381 | NSDictionary *queryParams = @{@"filter": [NSString stringWithFormat:@"contact_id=%@", contactId]}; 382 | 383 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_info"] method:@"DELETE" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 384 | } 385 | 386 | - (void)removeContactImageFolderWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 387 | { 388 | // delete all files and folders in the target folder 389 | NSDictionary *queryParams = @{@"force": @"1"}; 390 | 391 | [self callApiWithPath:[Routing resourceFolderWithFolderPath:contactId.stringValue] method:@"DELETE" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 392 | } 393 | 394 | - (void)getContactInfoFromServerWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 395 | { 396 | // create filter to get info only for this contact 397 | NSDictionary *queryParams = @{@"filter": [NSString stringWithFormat:@"contact_id=%@", contactId]}; 398 | 399 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_info"] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 400 | } 401 | 402 | - (void)getProfileImageFromServerWithContactId:(NSNumber *)contactId fileName:(NSString *)fileName success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 403 | { 404 | // request a download from the file 405 | NSDictionary *queryParams = @{@"include_properties": @"1", 406 | @"content": @"1", 407 | @"download": @"1"}; 408 | 409 | [self callApiWithPath:[Routing resourceFileWithFolderPath:contactId.stringValue fileName:fileName] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 410 | } 411 | 412 | - (void)getContactGroupsWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 413 | { 414 | // only get contact_group_relationships for this contact 415 | NSMutableDictionary *queryParams = [@{@"filter": [NSString stringWithFormat:@"contact_id=%@", contactId]} mutableCopy]; 416 | 417 | // request without related would return just {id, groupId, contactId} 418 | // set the related field to go get the group records referenced by 419 | // each contact_group_relationship record 420 | queryParams[@"related"] = @"contact_group_by_contact_group_id"; 421 | 422 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 423 | } 424 | 425 | - (void)addContactToServerWithDetails:(NSDictionary *)contactDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 426 | { 427 | // need to create contact first, then can add contactInfo and group relationships 428 | id body = [self toResourceArray:contactDetails]; 429 | 430 | [self callApiWithPath:[Routing serviceWithTableName:@"contact"] method:@"POST" queryParams:nil body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 431 | } 432 | 433 | - (void)addContactGroupRelationToServerWithContactId:(NSNumber *)contactId groupId:(NSNumber *)groupId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 434 | { 435 | // build request body 436 | // need to put in any extra field-key pair and avoid NSUrl timeout issue 437 | // otherwise it drops connection 438 | NSDictionary* bodyData = @{@"contact_group_id": groupId, 439 | @"contact_id": contactId}; 440 | id body = [self toResourceArray:bodyData]; 441 | 442 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_group_relationship"] method:@"POST" queryParams:nil body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 443 | } 444 | 445 | - (void)addContactInfoToServer:(NSArray *)info success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 446 | { 447 | id body = [self toResourceArrayFromArray:info]; 448 | 449 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_info"] method:@"POST" queryParams:nil body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 450 | } 451 | 452 | - (void)addContactImageWithContactId:(NSNumber *)contactId image:(UIImage *)image imageName:(NSString *)imageName success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 453 | { 454 | // first we need to create folder, then image 455 | 456 | [self callApiWithPath:[Routing resourceFolderWithFolderPath:contactId.stringValue] method:@"POST" queryParams:nil body:nil headerParams:self.sessionHeaderParams success:^(NSDictionary *response) { 457 | 458 | [self putImageToFolderWithPath:contactId.stringValue image:image fileName:imageName success:successBlock failure:failureBlock]; 459 | 460 | } failure:failureBlock]; 461 | } 462 | 463 | - (void)putImageToFolderWithPath:(NSString *)folderPath image:(UIImage *)image fileName:(NSString *)fileName success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 464 | { 465 | NSData *imageData = UIImageJPEGRepresentation(image, 0.1); 466 | NIKFile *file = [[NIKFile alloc] initWithNameData:fileName mimeType:@"application/octet-stream" data:imageData]; 467 | 468 | [self callApiWithPath:[Routing resourceFileWithFolderPath:folderPath fileName:fileName] method:@"POST" queryParams:nil body:file headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 469 | } 470 | 471 | - (void)updateContactWithContactId:(NSNumber *)contactId contactDetails:(NSDictionary *)contactDetails success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 472 | { 473 | // set the id of the contact we are looking at 474 | NSDictionary *queryParams = @{@"ids": contactId.stringValue}; 475 | id body = [self toResourceArray:contactDetails]; 476 | 477 | [self callApiWithPath:[Routing serviceWithTableName:@"contact"] method:@"PATCH" queryParams:queryParams body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 478 | } 479 | 480 | - (void)updateContactInfo: (NSArray *)info success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 481 | { 482 | id body = [self toResourceArrayFromArray:info]; 483 | 484 | [self callApiWithPath:[Routing serviceWithTableName:@"contact_info"] method:@"PATCH" queryParams:nil body:body headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 485 | } 486 | 487 | - (void)getImageListFromServerWithContactId:(NSNumber *)contactId success:(SuccessBlock)successBlock failure:(FailureBlock)failureBlock 488 | { 489 | // only want to get files, not any sub folders 490 | NSDictionary *queryParams = @{@"include_folders": @"0", 491 | @"include_files": @"1"}; 492 | 493 | [self callApiWithPath:[Routing resourceFolderWithFolderPath:contactId.stringValue] method:@"GET" queryParams:queryParams body:nil headerParams:self.sessionHeaderParams success:successBlock failure:failureBlock]; 494 | } 495 | 496 | @end 497 | -------------------------------------------------------------------------------- /example-ios/SampleApp/RegisterViewController.h: -------------------------------------------------------------------------------- 1 | #ifndef example_ios_RegisterViewController_h 2 | #define example_ios_RegisterViewController_h 3 | 4 | #import 5 | 6 | @interface RegisterViewController : UIViewController 7 | 8 | @property (weak, nonatomic) IBOutlet UITextField *emailTextField; 9 | @property (weak, nonatomic) IBOutlet UITextField *passwordTextField; 10 | 11 | - (IBAction)SubmitActionEvent:(id)sender; 12 | 13 | @end 14 | #endif 15 | -------------------------------------------------------------------------------- /example-ios/SampleApp/RegisterViewController.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "RegisterViewController.h" 4 | #import "AddressBookViewController.h" 5 | 6 | #import "RESTEngine.h" 7 | #import "AppDelegate.h" 8 | 9 | @interface RegisterViewController () 10 | 11 | @end 12 | 13 | @implementation RegisterViewController 14 | 15 | - (void) viewDidLoad { 16 | [super viewDidLoad]; 17 | 18 | [self.emailTextField setValue:[UIColor colorWithRed:180/255.0 green:180/255.0 blue:180/255.0 alpha:1.0] forKeyPath:@"_placeholderLabel.textColor"]; 19 | [self.passwordTextField setValue:[UIColor colorWithRed:180/255.0 green:180/255.0 blue:180/255.0 alpha:1.0] forKeyPath:@"_placeholderLabel.textColor"]; 20 | 21 | } 22 | 23 | - (void) viewWillAppear:(BOOL)animated{ 24 | [super viewWillAppear:animated]; 25 | 26 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 27 | CustomNavBar* navBar = bar.globalToolBar; 28 | [navBar showBackButton:YES]; 29 | [navBar showAddButton:NO]; 30 | [navBar showEditButton:NO]; 31 | [navBar.backButton addTarget:self action:@selector(hitBackButton) forControlEvents:UIControlEventTouchDown]; 32 | } 33 | 34 | - (void) viewWillDisappear:(BOOL)animated { 35 | [super viewWillDisappear:animated]; 36 | AppDelegate* bar = [[UIApplication sharedApplication] delegate]; 37 | CustomNavBar* navBar = bar.globalToolBar; 38 | [navBar.backButton removeTarget:self action:@selector(hitBackButton) forControlEvents:UIControlEventTouchDown]; 39 | 40 | } 41 | 42 | - (void) hitBackButton { 43 | [self.navigationController popViewControllerAnimated:YES]; 44 | } 45 | 46 | - (IBAction)SubmitActionEvent:(id)sender { 47 | [self.view endEditing:YES]; 48 | if(self.emailTextField.text.length>0 && self.passwordTextField.text.length>0){ 49 | 50 | [[RESTEngine sharedEngine] registerWithEmail:self.emailTextField.text password:self.passwordTextField.text success:^(NSDictionary *response) { 51 | 52 | [RESTEngine sharedEngine].sessionToken = response[@"session_token"]; 53 | [[NSUserDefaults standardUserDefaults] setValue:self.emailTextField.text forKey:kUserEmail]; 54 | [[NSUserDefaults standardUserDefaults] setValue:self.passwordTextField.text forKey:kPassword]; 55 | [[NSUserDefaults standardUserDefaults] synchronize]; 56 | dispatch_async(dispatch_get_main_queue(),^ (void){ 57 | [self showAddressBookViewController]; 58 | }); 59 | 60 | } failure:^(NSError *error) { 61 | NSLog(@"Error registering new user: %@",error); 62 | dispatch_async(dispatch_get_main_queue(),^ (void){ 63 | UIAlertView *message= [[UIAlertView alloc]initWithTitle:@"" message:error.errorMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 64 | [message show]; 65 | }); 66 | }]; 67 | } else { 68 | UIAlertView *message=[[UIAlertView alloc]initWithTitle:@"" message:@"Enter email and password" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 69 | [message show]; 70 | } 71 | } 72 | 73 | - (void) showAddressBookViewController { 74 | AddressBookViewController* addressBookViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AddressBookViewController"]; 75 | [self.navigationController pushViewController:addressBookViewController animated:YES]; 76 | } 77 | 78 | @end -------------------------------------------------------------------------------- /example-ios/SampleApp/address_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/SampleApp/address_book.png -------------------------------------------------------------------------------- /example-ios/SampleApp/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // example-ios 4 | // 5 | // Created by connor foody on 6/3/15. 6 | // Copyright (c) 2015 connor foody. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example-ios/SampleAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /example-ios/SampleAppTests/example_iosTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface example_iosTests : XCTestCase 5 | 6 | @end 7 | 8 | @implementation example_iosTests 9 | 10 | - (void)setUp { 11 | [super setUp]; 12 | // Put setup code here. This method is called before the invocation of each test method in the class. 13 | } 14 | 15 | - (void)tearDown { 16 | // Put teardown code here. This method is called after the invocation of each test method in the class. 17 | [super tearDown]; 18 | } 19 | 20 | - (void)testExample { 21 | // This is an example of a functional test case. 22 | XCTAssert(YES, @"Pass"); 23 | } 24 | 25 | - (void)testPerformanceExample { 26 | // This is an example of a performance test case. 27 | [self measureBlock:^{ 28 | // Put the code you want to measure the time of here. 29 | }]; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /example-ios/api/NIKApiInvoker.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | 4 | @interface NIKApiInvoker : NSObject { 5 | 6 | @private 7 | NSOperationQueue *_queue; 8 | } 9 | 10 | @property(nonatomic, readonly) NSOperationQueue* queue; 11 | @property(nonatomic, assign) NSURLRequestCachePolicy cachePolicy; 12 | 13 | /* 14 | * get the shared singleton 15 | */ 16 | + (NIKApiInvoker*)sharedInstance; 17 | 18 | /* 19 | * primary way to access and use the API 20 | * builds and sends an async NSUrl request 21 | * 22 | * @param path url to service, general form is /api/v2// 23 | * @param method http verb 24 | * @param querryParams varies by call, can be put into path instead of here 25 | * @param body request body, varies by call 26 | * @param headerParams user should pass in the app api key and a session token 27 | * @param contentType json or xml 28 | * @param completionBlock block to be executed once call is done 29 | */ 30 | -(void) restPath: (NSString*) path 31 | method: (NSString*) method 32 | queryParams: (NSDictionary*) queryParams 33 | body: (id)body 34 | headerParams: (NSDictionary*) headerParams 35 | contentType: (NSString*) contentType 36 | completionBlock: (void (^)(NSDictionary*, NSError *))completionBlock; 37 | 38 | @end 39 | 40 | @interface NIKRequestBuilder : NSObject 41 | /* 42 | * Builds NSURLRequests with the format for the DreamFactory Rest API 43 | * 44 | * This will play nice if you want to roll your own set up or use a 45 | * third party library like AFNetworking to send the REST requests 46 | * 47 | * @param path url to service, general form is /api/v2// 48 | * @param method http verb 49 | * @param querryParams varies by call, can be put into path instead of here 50 | * @param body request body, varies by call 51 | * @param headerParams user should pass in the app api key and a session token 52 | * @param contentType json or xml 53 | * @param completionBlock block to be executed once call is done 54 | */ 55 | + (NSURLRequest*) restPath: (NSString*) path 56 | method: (NSString*) method 57 | queryParams: (NSDictionary*) queryParams 58 | body: (id)body 59 | headerParams: (NSDictionary*) headerParams 60 | contentType: (NSString*) contentType; 61 | 62 | /* 63 | * Formats a string to be escaped. Used to put query params into the 64 | * request url 65 | */ 66 | + (NSString*) escapeString:(NSString *)unescaped; 67 | @end -------------------------------------------------------------------------------- /example-ios/api/NIKApiInvoker.m: -------------------------------------------------------------------------------- 1 | #import "NIKApiInvoker.h" 2 | #import 3 | #import "NIKFile.h" 4 | 5 | @implementation NIKApiInvoker 6 | 7 | @synthesize queue = _queue; 8 | 9 | static NSInteger __LoadingObjectsCount = 0; 10 | 11 | + (NIKApiInvoker*)sharedInstance { 12 | static NIKApiInvoker *_sharedInstance = nil; 13 | static dispatch_once_t onceToken; 14 | dispatch_once(&onceToken, ^{ 15 | _sharedInstance = [[self alloc] init]; 16 | }); 17 | return _sharedInstance; 18 | } 19 | 20 | - (void)updateLoadCountWithDelta:(NSInteger)countDelta { 21 | @synchronized(self) { 22 | __LoadingObjectsCount += countDelta; 23 | __LoadingObjectsCount = (__LoadingObjectsCount < 0) ? 0 : __LoadingObjectsCount ; 24 | 25 | #if TARGET_OS_IPHONE 26 | [UIApplication sharedApplication].networkActivityIndicatorVisible = __LoadingObjectsCount > 0; 27 | #endif 28 | } 29 | } 30 | 31 | - (void)startLoad { 32 | [self updateLoadCountWithDelta:1]; 33 | } 34 | 35 | - (void)stopLoad { 36 | [self updateLoadCountWithDelta:-1]; 37 | } 38 | 39 | 40 | - (id) init { 41 | self = [super init]; 42 | _queue = [[NSOperationQueue alloc] init]; 43 | _cachePolicy = NSURLRequestUseProtocolCachePolicy; 44 | 45 | return self; 46 | } 47 | 48 | -(void) restPath: (NSString*) path 49 | method: (NSString*) method 50 | queryParams: (NSDictionary*) queryParams 51 | body: (id) body 52 | headerParams: (NSDictionary*) headerParams 53 | contentType: (NSString*) contentType 54 | completionBlock: (void (^)(NSDictionary*, NSError *))completionBlock 55 | { 56 | NSURLRequest* request = [NIKRequestBuilder restPath:path 57 | method:method 58 | queryParams:queryParams 59 | body:body 60 | headerParams:headerParams 61 | contentType:contentType]; 62 | 63 | /******************************************************************* 64 | * 65 | * NOTE: apple added App Transport Security in iOS 9.0+ to improve 66 | * security. As of this writing (7/15) all plain text http 67 | * connections fail by default. For more info about App 68 | * Transport Security and how to handle this issue here: 69 | * https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/index.html 70 | * 71 | *******************************************************************/ 72 | 73 | // Handle caching on GET requests 74 | if ((_cachePolicy == NSURLRequestReturnCacheDataElseLoad || _cachePolicy == NSURLRequestReturnCacheDataDontLoad) && [method isEqualToString:@"GET"]) { 75 | NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; 76 | NSData *data = [cacheResponse data]; 77 | if (data) { 78 | NSError *error = nil; 79 | NSDictionary* results = [NSJSONSerialization 80 | JSONObjectWithData:data 81 | options:kNilOptions 82 | error:&error]; 83 | completionBlock(results, nil); 84 | } 85 | } 86 | 87 | if (_cachePolicy == NSURLRequestReturnCacheDataDontLoad){ 88 | return; 89 | } 90 | [self startLoad]; // for network activity indicator 91 | 92 | NSDate *date = [NSDate date]; 93 | [NSURLConnection sendAsynchronousRequest:request 94 | queue:self.queue 95 | completionHandler: 96 | ^(NSURLResponse *response, NSData *response_data, NSError *response_error) { 97 | [self stopLoad]; 98 | long statusCode = [(NSHTTPURLResponse*)response statusCode]; 99 | if (response_error) { 100 | if (response_data) { 101 | NSDictionary* results = [NSJSONSerialization 102 | JSONObjectWithData:response_data 103 | options:kNilOptions 104 | error:nil]; 105 | if (results != nil) { 106 | completionBlock(nil, [NSError errorWithDomain:response_error.domain code:response_error.code userInfo:results]); 107 | } else { 108 | completionBlock(nil, response_error); 109 | } 110 | } else { 111 | completionBlock(nil, response_error); 112 | } 113 | return; 114 | } 115 | else if (!NSLocationInRange(statusCode, NSMakeRange(200, 99))){ 116 | response_error = [NSError errorWithDomain:@"swagger" 117 | code:statusCode 118 | userInfo:[NSJSONSerialization JSONObjectWithData:response_data 119 | options:kNilOptions 120 | error:&response_error]]; 121 | 122 | completionBlock(nil, response_error); 123 | return; 124 | } 125 | else { 126 | NSDictionary* results = [NSJSONSerialization 127 | JSONObjectWithData:response_data 128 | options:kNilOptions 129 | error:&response_error]; 130 | 131 | if ([[NSUserDefaults standardUserDefaults] boolForKey:@"RVBLogging"]) { 132 | NSLog(@"fetched results (%f seconds): %@", [[NSDate date] timeIntervalSinceDate:date], results); 133 | } 134 | completionBlock(results, nil); 135 | } 136 | }]; 137 | } 138 | @end 139 | 140 | @implementation NIKRequestBuilder 141 | 142 | + (NSURLRequest *) restPath:(NSString *)path 143 | method:(NSString *)method 144 | queryParams:(NSDictionary *)queryParams 145 | body:(id)body 146 | headerParams:(NSDictionary *)headerParams 147 | contentType:(NSString *)contentType 148 | { 149 | 150 | NSMutableURLRequest* request = [[NSMutableURLRequest alloc] init]; 151 | @autoreleasepool { 152 | NSMutableString * requestUrl = [NSMutableString stringWithFormat:@"%@", path]; 153 | NSString * separator = nil; 154 | int counter = 0; 155 | if(queryParams != nil){ 156 | // build the query params into the URL 157 | // ie @"filter" = "id=5" becomes "?filter=id=5 158 | for(NSString * key in [queryParams keyEnumerator]){ 159 | @autoreleasepool { 160 | if(counter == 0) separator = @"?"; 161 | else separator = @"&"; 162 | NSString * value; 163 | if([[queryParams valueForKey:key] isKindOfClass:[NSString class]]){ 164 | value = [NIKRequestBuilder escapeString:[queryParams valueForKey:key]]; 165 | } 166 | else { 167 | value = [NSString stringWithFormat:@"%@", [queryParams valueForKey:key]]; 168 | } 169 | [requestUrl appendString:[NSString stringWithFormat:@"%@%@=%@", separator, 170 | [self escapeString:key], value]]; 171 | counter += 1; 172 | } 173 | } 174 | } 175 | 176 | if ([[NSUserDefaults standardUserDefaults] boolForKey:@"RVBLogging"]) { 177 | NSLog(@"request url: %@", requestUrl); 178 | } 179 | 180 | NSURL* URL = [NSURL URLWithString:requestUrl]; 181 | [request setURL:URL]; 182 | // The cache settings get set by the ApiInvoker 183 | [request setTimeoutInterval:30]; 184 | 185 | if(headerParams != nil){ 186 | for(NSString * key in [headerParams keyEnumerator]){ 187 | @autoreleasepool { 188 | [request setValue:[headerParams valueForKey:key] forHTTPHeaderField:key]; 189 | } 190 | } 191 | } 192 | 193 | [request setHTTPMethod:method]; 194 | if(body != nil) { 195 | // build the body into JSON 196 | NSError* __autoreleasing error = nil; 197 | NSData * data = nil; 198 | if([body isKindOfClass:[NSDictionary class]]){ 199 | data = [NSJSONSerialization dataWithJSONObject:body 200 | options:kNilOptions error:&error]; 201 | } 202 | else if ([body isKindOfClass:[NIKFile class]]){ 203 | NIKFile * file = (NIKFile*) body; 204 | data = file.data; 205 | } 206 | else if ([body isKindOfClass:[NSArray class]]){ 207 | data = [NSJSONSerialization dataWithJSONObject:body 208 | options:kNilOptions error:&error]; 209 | } 210 | else { 211 | data = [body dataUsingEncoding:NSUTF8StringEncoding]; 212 | } 213 | NSString *postLength = [NSString stringWithFormat:@"%lu", (unsigned long)[data length]]; 214 | [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; 215 | [request setHTTPBody:data]; 216 | 217 | [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; 218 | } 219 | } 220 | return request; 221 | } 222 | 223 | + (NSString*) escapeString:(NSString *)unescaped { 224 | // bridge CF obj to get ARC and release to cancel retain from create 225 | return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes( 226 | NULL, 227 | (__bridge CFStringRef) unescaped, 228 | NULL, 229 | (CFStringRef)@"!*'();:@&=+$,/?%#[]", 230 | kCFStringEncodingUTF8)); 231 | } 232 | @end -------------------------------------------------------------------------------- /example-ios/api/NIKFile.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /* 4 | * Use this object when building a request with a file. Pass it 5 | * in as the body of the request to ensure that the file is built 6 | * and sent up properly, especially with images. 7 | */ 8 | @interface NIKFile : NSObject { 9 | 10 | @private 11 | NSString* name; 12 | NSString* mimeType; 13 | NSData* data; 14 | } 15 | 16 | @property(nonatomic, readonly) NSString* name; 17 | @property(nonatomic, readonly) NSString* mimeType; 18 | @property(nonatomic, readonly) NSData* data; 19 | 20 | - (id) initWithNameData: (NSString*) filename 21 | mimeType: (NSString*) mimeType 22 | data: (NSData*) data; 23 | 24 | @end -------------------------------------------------------------------------------- /example-ios/api/NIKFile.m: -------------------------------------------------------------------------------- 1 | #import "NIKFile.h" 2 | 3 | @implementation NIKFile 4 | 5 | @synthesize name = _name; 6 | @synthesize mimeType = _mimeType; 7 | @synthesize data = _data; 8 | 9 | - (id) init { 10 | self = [super init]; 11 | return self; 12 | } 13 | 14 | - (id) initWithNameData: (NSString*) filename 15 | mimeType: (NSString*) fileMimeType 16 | data: (NSData*) datat { 17 | self = [super init]; 18 | if(self) { 19 | _name = filename; 20 | _mimeType = fileMimeType; 21 | _data = datat; 22 | } 23 | return self; 24 | } 25 | 26 | @end -------------------------------------------------------------------------------- /example-ios/package/add_ios.dfpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-sdk/1bc0c793cba09efa3d9aed9d8b5e672dcd291708/example-ios/package/add_ios.dfpkg -------------------------------------------------------------------------------- /example-ios/package/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Address Book for iOS", 3 | "description": "An address book app for iOS showing user registration, user login, and CRUD.", 4 | "type": 0, 5 | "is_active": true 6 | } -------------------------------------------------------------------------------- /example-ios/package/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": [ 3 | { 4 | "name": "db", 5 | "table": [ 6 | { 7 | "description": "The main table for tracking contacts.", 8 | "name": "contact", 9 | "field": [ 10 | { 11 | "name": "id", 12 | "label": "Contact Id", 13 | "type": "id" 14 | }, 15 | { 16 | "name": "first_name", 17 | "type": "string", 18 | "size": 40, 19 | "allow_null": false, 20 | "is_index": true, 21 | "validation": { 22 | "not_empty": { 23 | "on_fail": "First name value must not be empty." 24 | } 25 | } 26 | }, 27 | { 28 | "name": "last_name", 29 | "type": "string", 30 | "size": 40, 31 | "allow_null": false, 32 | "is_index": true, 33 | "validation": { 34 | "not_empty": { 35 | "on_fail": "Last name value must not be empty." 36 | } 37 | } 38 | }, 39 | { 40 | "name": "image_url", 41 | "label": "image_url", 42 | "type": "text", 43 | "validation": { 44 | "url": { 45 | "on_fail": "Not a valid URL value." 46 | } 47 | }, 48 | "allow_null": true 49 | }, 50 | { 51 | "name": "twitter", 52 | "label": "Twitter Handle", 53 | "type": "string", 54 | "size": 18, 55 | "allow_null": true 56 | }, 57 | { 58 | "name": "skype", 59 | "label": "Skype Account", 60 | "type": "string", 61 | "size": 255, 62 | "allow_null": true 63 | }, 64 | { 65 | "name": "notes", 66 | "label": "notes", 67 | "type": "text", 68 | "allow_null": true 69 | } 70 | ] 71 | }, 72 | { 73 | "description": "The contact details sub-table, owned by contact table row.", 74 | "name": "contact_info", 75 | "field": [ 76 | { 77 | "name": "id", 78 | "label": "Info Id", 79 | "type": "id" 80 | }, 81 | { 82 | "name": "ordinal", 83 | "type": "integer", 84 | "allow_null": true 85 | }, 86 | { 87 | "name": "contact_id", 88 | "type": "reference", 89 | "allow_null": false, 90 | "ref_table": "contact", 91 | "ref_fields": "id", 92 | "ref_on_delete": "CASCADE" 93 | }, 94 | { 95 | "name": "info_type", 96 | "type": "string", 97 | "size": 32, 98 | "allow_null": false, 99 | "validation": { 100 | "not_empty": { 101 | "on_fail": "Information type can not be empty." 102 | }, 103 | "picklist": { 104 | "on_fail": "Not a valid information type." 105 | } 106 | }, 107 | "picklist": [ 108 | "work", 109 | "home", 110 | "mobile", 111 | "other" 112 | ] 113 | }, 114 | { 115 | "name": "phone", 116 | "label": "Phone Number", 117 | "type": "string", 118 | "size": 32 119 | }, 120 | { 121 | "name": "email", 122 | "label": "Email Address", 123 | "type": "string", 124 | "size": 255, 125 | "validation": { 126 | "email": { 127 | "on_fail": "Not a valid email address." 128 | } 129 | } 130 | }, 131 | { 132 | "name": "address", 133 | "label": "Street Address", 134 | "type": "string" 135 | }, 136 | { 137 | "name": "city", 138 | "label": "city", 139 | "type": "string", 140 | "size": 64 141 | }, 142 | { 143 | "name": "state", 144 | "label": "state", 145 | "type": "string", 146 | "size": 64 147 | }, 148 | { 149 | "name": "zip", 150 | "label": "zip", 151 | "type": "string", 152 | "size": 16 153 | }, 154 | { 155 | "name": "country", 156 | "label": "country", 157 | "type": "string", 158 | "size": 64 159 | } 160 | ] 161 | }, 162 | { 163 | "description": "The main table for tracking groups of contact.", 164 | "name": "contact_group", 165 | "field": [ 166 | { 167 | "name": "id", 168 | "type": "id" 169 | }, 170 | { 171 | "name": "name", 172 | "type": "string", 173 | "size": 128, 174 | "allow_null": false, 175 | "validation": { 176 | "not_empty": { 177 | "on_fail": "Group name value must not be empty." 178 | } 179 | } 180 | } 181 | ] 182 | }, 183 | { 184 | "description": "The join table for tracking contacts in groups.", 185 | "name": "contact_group_relationship", 186 | "field": [ 187 | { 188 | "name": "id", 189 | "type": "id" 190 | }, 191 | { 192 | "name": "contact_id", 193 | "type": "reference", 194 | "allow_null": false, 195 | "ref_table": "contact", 196 | "ref_fields": "id", 197 | "ref_on_delete": "CASCADE" 198 | }, 199 | { 200 | "name": "contact_group_id", 201 | "type": "reference", 202 | "allow_null": false, 203 | "ref_table": "contact_group", 204 | "ref_fields": "id", 205 | "ref_on_delete": "CASCADE" 206 | } 207 | ] 208 | } 209 | ] 210 | } 211 | ] 212 | } 213 | --------------------------------------------------------------------------------