├── .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 |
--------------------------------------------------------------------------------