├── .gitignore ├── README.md └── SampleAppSwift ├── API ├── NIKApiInvoker.swift └── NIKFile.swift ├── SampleAppSwift.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── tigr.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── tigr.xcuserdatad │ └── xcschemes │ ├── SampleAppSwift.xcscheme │ └── xcschememanagement.plist ├── SampleAppSwift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── DreamFactory-logo-horiz-filled.imageset │ │ ├── Contents.json │ │ └── DreamFactory-logo-horiz-filled.png │ ├── DreamFactory-logo-horiz.imageset │ │ ├── Contents.json │ │ └── DreamFactory-logo-horiz.png │ ├── chat.imageset │ │ ├── Contents.json │ │ └── chat.png │ ├── default_portrait.imageset │ │ ├── Contents.json │ │ └── default_portrait.png │ ├── home.imageset │ │ ├── Contents.json │ │ └── home.png │ ├── mail.imageset │ │ ├── Contents.json │ │ └── mail.png │ ├── phone1.imageset │ │ ├── Contents.json │ │ └── phone1.png │ ├── skype.imageset │ │ ├── Contents.json │ │ └── skype.png │ └── twitter2.imageset │ │ ├── Contents.json │ │ └── twitter2.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Controller │ ├── AddressBookViewController.swift │ ├── ContactEditViewController.swift │ ├── ContactListViewController.swift │ ├── ContactViewController.swift │ ├── GroupAddViewController.swift │ ├── MasterViewController.swift │ ├── ProfileImagePickerViewController.swift │ └── RegisterViewController.swift ├── Info.plist ├── Model │ ├── ContactDetailRecord.swift │ ├── ContactRecord.swift │ └── GroupRecord.swift ├── Network │ └── RESTEngine.swift ├── Utils │ └── UIUtils.swift └── View │ ├── ContactInfoView.swift │ ├── CustomNavBar.swift │ ├── PickerSelector.swift │ └── PickerSelector.xib └── package ├── add_ios_swift.dfpkg ├── data.json ├── description.json └── schema.json /.gitignore: -------------------------------------------------------------------------------- 1 | SampleAppSwift/SampleAppSwift.xcodeproj/project.xcworkspace/xcuserdata 2 | SampleAppSwift/SampleAppSwift.xcodeproj/xcuserdata 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Address Book for iOS Swift 2 | ========================== 3 | 4 | This repo contains a sample address book application for iOS Swift 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 Swift' 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 Network/RESTEngine.swift 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 Network/RESTEngine.swift. 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 | -------------------------------------------------------------------------------- /SampleAppSwift/API/NIKApiInvoker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIKApiInvoker.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class NIKApiInvoker { 12 | 13 | let queue = NSOperationQueue() 14 | let cachePolicy = NSURLRequestCachePolicy.UseProtocolCachePolicy 15 | 16 | /** 17 | get the shared singleton 18 | */ 19 | static let sharedInstance = NIKApiInvoker() 20 | static var __LoadingObjectsCount = 0 21 | private init() { 22 | } 23 | 24 | private func updateLoadCountWithDelta(countDelta: Int) { 25 | objc_sync_enter(self) 26 | NIKApiInvoker.__LoadingObjectsCount += countDelta 27 | NIKApiInvoker.__LoadingObjectsCount = max(0, NIKApiInvoker.__LoadingObjectsCount) 28 | 29 | UIApplication.sharedApplication().networkActivityIndicatorVisible = NIKApiInvoker.__LoadingObjectsCount > 0 30 | 31 | objc_sync_exit(self) 32 | } 33 | 34 | private func startLoad() { 35 | updateLoadCountWithDelta(1) 36 | } 37 | 38 | private func stopLoad() { 39 | updateLoadCountWithDelta(-1) 40 | } 41 | 42 | /** 43 | primary way to access and use the API 44 | builds and sends an async NSUrl request 45 | 46 | - Parameter path: url to service, general form is /api/v2// 47 | - Parameter method: http verb 48 | - Parameter queryParams: varies by call, can be put into path instead of here 49 | - Parameter body: request body, varies by call 50 | - Parameter headerParams: user should pass in the app api key and a session token 51 | - Parameter contentType: json or xml 52 | - Parameter completionBlock: block to be executed once call is done 53 | */ 54 | func restPath(path: String, method: String, queryParams: [String: AnyObject]?, body: AnyObject?, headerParams: [String: String]?, contentType: String?, completionBlock: ([String: AnyObject]?, NSError?) -> Void) { 55 | let request = NIKRequestBuilder.restPath(path, method: method, queryParams: queryParams, body: body, headerParams: headerParams, contentType: contentType) 56 | 57 | /******************************************************************* 58 | * 59 | * NOTE: apple added App Transport Security in iOS 9.0+ to improve 60 | * security. As of this writing (7/15) all plain text http 61 | * connections fail by default. For more info about App 62 | * Transport Security and how to handle this issue here: 63 | * https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/index.html 64 | * 65 | *******************************************************************/ 66 | 67 | // Handle caching on GET requests 68 | 69 | if (cachePolicy == .ReturnCacheDataElseLoad || cachePolicy == .ReturnCacheDataDontLoad) && method == "GET" { 70 | let cacheResponse = NSURLCache.sharedURLCache().cachedResponseForRequest(request) 71 | let data = cacheResponse?.data 72 | if let data = data { 73 | let results = try? NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] 74 | completionBlock(results!, nil) 75 | } 76 | } 77 | 78 | if cachePolicy == .ReturnCacheDataDontLoad { 79 | return 80 | } 81 | startLoad() // for network activity indicator 82 | 83 | let date = NSDate() 84 | NSURLConnection.sendAsynchronousRequest(request, queue: queue) {(response, response_data, response_error) -> Void in 85 | self.stopLoad() 86 | if let response_error = response_error { 87 | if let response_data = response_data { 88 | let results = try? NSJSONSerialization.JSONObjectWithData(response_data, options: []) 89 | if let results = results as? [String: AnyObject] { 90 | completionBlock(nil, NSError(domain: response_error.domain, code: response_error.code, userInfo: results)) 91 | } else { 92 | completionBlock(nil, response_error) 93 | } 94 | } else { 95 | completionBlock(nil, response_error) 96 | } 97 | return 98 | } else { 99 | let statusCode = (response as! NSHTTPURLResponse).statusCode 100 | if !NSLocationInRange(statusCode, NSMakeRange(200, 99)) { 101 | let response_error = NSError(domain: "swagger", code: statusCode, userInfo: try! NSJSONSerialization.JSONObjectWithData(response_data!, options: []) as? [NSObject: AnyObject]) 102 | completionBlock(nil, response_error) 103 | return 104 | } else { 105 | let results = try! NSJSONSerialization.JSONObjectWithData(response_data!, options: []) as! [String: AnyObject] 106 | if NSUserDefaults.standardUserDefaults().boolForKey("RVBLogging") { 107 | NSLog("fetched results (\(NSDate().timeIntervalSinceDate(date)) seconds): \(results)") 108 | } 109 | completionBlock(results, nil) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | final class NIKRequestBuilder { 117 | 118 | /** 119 | Builds NSURLRequests with the format for the DreamFactory Rest API 120 | 121 | This will play nice if you want to roll your own set up or use a 122 | third party library like AFNetworking to send the REST requests 123 | 124 | - Parameter path: url to service, general form is /api/v2// 125 | - Parameter method: http verb 126 | - Parameter queryParams: varies by call, can be put into path instead of here 127 | - Parameter body: request body, varies by call 128 | - Parameter headerParams: user should pass in the app api key and a session token 129 | - Parameter contentType: json or xml 130 | */ 131 | static func restPath(path: String, method: String, queryParams: [String: AnyObject]?, body: AnyObject?, headerParams: [String: String]?, contentType: String?) -> NSURLRequest { 132 | let request = NSMutableURLRequest() 133 | var requestUrl = path 134 | if let queryParams = queryParams { 135 | // build the query params into the URL 136 | // ie @"filter" = "id=5" becomes "?filter=id=5 137 | let parameterString = queryParams.stringFromHttpParameters() 138 | requestUrl = "\(path)?\(parameterString)" 139 | } 140 | 141 | if NSUserDefaults.standardUserDefaults().boolForKey("RVBLogging") { 142 | NSLog("request url: \(requestUrl)") 143 | } 144 | 145 | let URL = NSURL(string: requestUrl)! 146 | request.URL = URL 147 | // The cache settings get set by the ApiInvoker 148 | request.timeoutInterval = 30 149 | 150 | if let headerParams = headerParams { 151 | for (key, value) in headerParams { 152 | request.setValue(value, forHTTPHeaderField: key) 153 | } 154 | } 155 | 156 | request.HTTPMethod = method 157 | if let body = body { 158 | // build the body into JSON 159 | var data: NSData! 160 | if body is [String: AnyObject] || body is [AnyObject] { 161 | data = try? NSJSONSerialization.dataWithJSONObject(body, options: []) 162 | } else if let body = body as? NIKFile { 163 | data = body.data 164 | } else { 165 | data = body.dataUsingEncoding(NSUTF8StringEncoding) 166 | } 167 | let postLength = "\(data.length)" 168 | request.setValue(postLength, forHTTPHeaderField: "Content-Length") 169 | request.HTTPBody = data 170 | request.setValue(contentType, forHTTPHeaderField: "Content-Type") 171 | } 172 | 173 | return request 174 | } 175 | } 176 | 177 | extension String { 178 | 179 | /** Percent escape value to be added to a URL query value as specified in RFC 3986 180 | - Returns: Percent escaped string. 181 | */ 182 | 183 | func stringByAddingPercentEncodingForURLQueryValue() -> String? { 184 | let characterSet = NSMutableCharacterSet.alphanumericCharacterSet() 185 | characterSet.addCharactersInString("-._~") 186 | 187 | return self.stringByAddingPercentEncodingWithAllowedCharacters(characterSet) 188 | } 189 | } 190 | 191 | extension Dictionary { 192 | 193 | /** Build string representation of HTTP parameter dictionary of keys and objects 194 | This percent escapes in compliance with RFC 3986 195 | - Returns: String representation in the form of key1=value1&key2=value2 where the keys and values are percent escaped 196 | */ 197 | 198 | func stringFromHttpParameters() -> String { 199 | let parameterArray = self.map { (key, value) -> String in 200 | let percentEscapedKey = (key as! String).stringByAddingPercentEncodingForURLQueryValue()! 201 | let percentEscapedValue = (value as! String).stringByAddingPercentEncodingForURLQueryValue()! 202 | return "\(percentEscapedKey)=\(percentEscapedValue)" 203 | } 204 | 205 | return parameterArray.joinWithSeparator("&") 206 | } 207 | } -------------------------------------------------------------------------------- /SampleAppSwift/API/NIKFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIKFile.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | Use this object when building a request with a file. Pass it 13 | in as the body of the request to ensure that the file is built 14 | and sent up properly, especially with images. 15 | */ 16 | class NIKFile { 17 | let name: String 18 | let mimeType: String 19 | let data: NSData 20 | 21 | init(name: String, mimeType: String, data: NSData) { 22 | self.name = name 23 | self.mimeType = mimeType 24 | self.data = data 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 824512211C3B76A6000EF52E /* GroupAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824512201C3B76A6000EF52E /* GroupAddViewController.swift */; }; 11 | 824512231C3B76C9000EF52E /* ContactListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824512221C3B76C9000EF52E /* ContactListViewController.swift */; }; 12 | 824512251C3BCC6E000EF52E /* ContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824512241C3BCC6E000EF52E /* ContactViewController.swift */; }; 13 | 824512271C3BF40A000EF52E /* ContactEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824512261C3BF40A000EF52E /* ContactEditViewController.swift */; }; 14 | 824512291C3BF6E4000EF52E /* ProfileImagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824512281C3BF6E4000EF52E /* ProfileImagePickerViewController.swift */; }; 15 | 82B39FF81C3A5B4600D4F23F /* CustomNavBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B39FF71C3A5B4600D4F23F /* CustomNavBar.swift */; }; 16 | 82B39FFD1C3A9B7300D4F23F /* ContactInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B39FFC1C3A9B7300D4F23F /* ContactInfoView.swift */; }; 17 | 82B39FFF1C3AA42E00D4F23F /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B39FFE1C3AA42E00D4F23F /* MasterViewController.swift */; }; 18 | 82B3A0011C3AA6EE00D4F23F /* RegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B3A0001C3AA6EE00D4F23F /* RegisterViewController.swift */; }; 19 | 82B3A0031C3AA70D00D4F23F /* AddressBookViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B3A0021C3AA70D00D4F23F /* AddressBookViewController.swift */; }; 20 | 82C7AEDF1C403D5C00E6909F /* RESTEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C7AEDE1C403D5C00E6909F /* RESTEngine.swift */; }; 21 | 82CABEAA1C49490400AE8E85 /* PickerSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82CABEA91C49490400AE8E85 /* PickerSelector.swift */; }; 22 | 82CABEAC1C49493800AE8E85 /* PickerSelector.xib in Resources */ = {isa = PBXBuildFile; fileRef = 82CABEAB1C49493800AE8E85 /* PickerSelector.xib */; }; 23 | 82CABEAF1C495F6D00AE8E85 /* UIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82CABEAE1C495F6D00AE8E85 /* UIUtils.swift */; }; 24 | 82D0A0F41C39986000DC41FB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D0A0F31C39986000DC41FB /* AppDelegate.swift */; }; 25 | 82D0A0FB1C39986000DC41FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 82D0A0FA1C39986000DC41FB /* Assets.xcassets */; }; 26 | 82D0A0FE1C39986000DC41FB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 82D0A0FC1C39986000DC41FB /* LaunchScreen.storyboard */; }; 27 | 82D0A1071C3998E800DC41FB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 82D0A1051C3998E800DC41FB /* Main.storyboard */; }; 28 | 82D0A10D1C39ADCC00DC41FB /* NIKApiInvoker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D0A10C1C39ADCC00DC41FB /* NIKApiInvoker.swift */; }; 29 | 82D0A10F1C39B9BA00DC41FB /* NIKFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D0A10E1C39B9BA00DC41FB /* NIKFile.swift */; }; 30 | 82D0A1111C39C9B600DC41FB /* GroupRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D0A1101C39C9B600DC41FB /* GroupRecord.swift */; }; 31 | 82D0A1131C39CA0F00DC41FB /* ContactRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D0A1121C39CA0F00DC41FB /* ContactRecord.swift */; }; 32 | 82D0A1151C39CA2100DC41FB /* ContactDetailRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D0A1141C39CA2100DC41FB /* ContactDetailRecord.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 824512201C3B76A6000EF52E /* GroupAddViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAddViewController.swift; sourceTree = ""; }; 37 | 824512221C3B76C9000EF52E /* ContactListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactListViewController.swift; sourceTree = ""; }; 38 | 824512241C3BCC6E000EF52E /* ContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = ""; }; 39 | 824512261C3BF40A000EF52E /* ContactEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactEditViewController.swift; sourceTree = ""; }; 40 | 824512281C3BF6E4000EF52E /* ProfileImagePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileImagePickerViewController.swift; sourceTree = ""; }; 41 | 82B39FF71C3A5B4600D4F23F /* CustomNavBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomNavBar.swift; sourceTree = ""; }; 42 | 82B39FFC1C3A9B7300D4F23F /* ContactInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactInfoView.swift; sourceTree = ""; }; 43 | 82B39FFE1C3AA42E00D4F23F /* MasterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; 44 | 82B3A0001C3AA6EE00D4F23F /* RegisterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegisterViewController.swift; sourceTree = ""; }; 45 | 82B3A0021C3AA70D00D4F23F /* AddressBookViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressBookViewController.swift; sourceTree = ""; }; 46 | 82C7AEDE1C403D5C00E6909F /* RESTEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTEngine.swift; sourceTree = ""; }; 47 | 82CABEA91C49490400AE8E85 /* PickerSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerSelector.swift; sourceTree = ""; }; 48 | 82CABEAB1C49493800AE8E85 /* PickerSelector.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PickerSelector.xib; sourceTree = ""; }; 49 | 82CABEAE1C495F6D00AE8E85 /* UIUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIUtils.swift; sourceTree = ""; }; 50 | 82D0A0F01C39986000DC41FB /* SampleAppSwift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleAppSwift.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 82D0A0F31C39986000DC41FB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 52 | 82D0A0FA1C39986000DC41FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | 82D0A0FD1C39986000DC41FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 54 | 82D0A0FF1C39986000DC41FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | 82D0A1061C3998E800DC41FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 82D0A10C1C39ADCC00DC41FB /* NIKApiInvoker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NIKApiInvoker.swift; sourceTree = ""; }; 57 | 82D0A10E1C39B9BA00DC41FB /* NIKFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NIKFile.swift; sourceTree = ""; }; 58 | 82D0A1101C39C9B600DC41FB /* GroupRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupRecord.swift; sourceTree = ""; }; 59 | 82D0A1121C39CA0F00DC41FB /* ContactRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRecord.swift; sourceTree = ""; }; 60 | 82D0A1141C39CA2100DC41FB /* ContactDetailRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactDetailRecord.swift; sourceTree = ""; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 82D0A0ED1C39986000DC41FB /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 82C7AEDD1C403CDA00E6909F /* Network */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 82C7AEDE1C403D5C00E6909F /* RESTEngine.swift */, 78 | ); 79 | path = Network; 80 | sourceTree = ""; 81 | }; 82 | 82CABEAD1C495F5B00AE8E85 /* Utils */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 82CABEAE1C495F6D00AE8E85 /* UIUtils.swift */, 86 | ); 87 | path = Utils; 88 | sourceTree = ""; 89 | }; 90 | 82D0A0E71C39986000DC41FB = { 91 | isa = PBXGroup; 92 | children = ( 93 | 82D0A10B1C39AD7800DC41FB /* API */, 94 | 82D0A0F21C39986000DC41FB /* SampleAppSwift */, 95 | 82D0A0F11C39986000DC41FB /* Products */, 96 | ); 97 | sourceTree = ""; 98 | }; 99 | 82D0A0F11C39986000DC41FB /* Products */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 82D0A0F01C39986000DC41FB /* SampleAppSwift.app */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | 82D0A0F21C39986000DC41FB /* SampleAppSwift */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 82D0A0F31C39986000DC41FB /* AppDelegate.swift */, 111 | 82D0A1051C3998E800DC41FB /* Main.storyboard */, 112 | 82C7AEDD1C403CDA00E6909F /* Network */, 113 | 82D0A1091C39AD6600DC41FB /* Model */, 114 | 82D0A10A1C39AD6600DC41FB /* View */, 115 | 82D0A1081C39AD6600DC41FB /* Controller */, 116 | 82CABEAD1C495F5B00AE8E85 /* Utils */, 117 | 82D0A0FA1C39986000DC41FB /* Assets.xcassets */, 118 | 82D0A0FC1C39986000DC41FB /* LaunchScreen.storyboard */, 119 | 82D0A0FF1C39986000DC41FB /* Info.plist */, 120 | ); 121 | path = SampleAppSwift; 122 | sourceTree = ""; 123 | }; 124 | 82D0A1081C39AD6600DC41FB /* Controller */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 82B39FFE1C3AA42E00D4F23F /* MasterViewController.swift */, 128 | 82B3A0001C3AA6EE00D4F23F /* RegisterViewController.swift */, 129 | 82B3A0021C3AA70D00D4F23F /* AddressBookViewController.swift */, 130 | 824512201C3B76A6000EF52E /* GroupAddViewController.swift */, 131 | 824512221C3B76C9000EF52E /* ContactListViewController.swift */, 132 | 824512241C3BCC6E000EF52E /* ContactViewController.swift */, 133 | 824512261C3BF40A000EF52E /* ContactEditViewController.swift */, 134 | 824512281C3BF6E4000EF52E /* ProfileImagePickerViewController.swift */, 135 | ); 136 | path = Controller; 137 | sourceTree = ""; 138 | }; 139 | 82D0A1091C39AD6600DC41FB /* Model */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 82D0A1101C39C9B600DC41FB /* GroupRecord.swift */, 143 | 82D0A1121C39CA0F00DC41FB /* ContactRecord.swift */, 144 | 82D0A1141C39CA2100DC41FB /* ContactDetailRecord.swift */, 145 | ); 146 | path = Model; 147 | sourceTree = ""; 148 | }; 149 | 82D0A10A1C39AD6600DC41FB /* View */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 82B39FF71C3A5B4600D4F23F /* CustomNavBar.swift */, 153 | 82B39FFC1C3A9B7300D4F23F /* ContactInfoView.swift */, 154 | 82CABEA91C49490400AE8E85 /* PickerSelector.swift */, 155 | 82CABEAB1C49493800AE8E85 /* PickerSelector.xib */, 156 | ); 157 | path = View; 158 | sourceTree = ""; 159 | }; 160 | 82D0A10B1C39AD7800DC41FB /* API */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 82D0A10C1C39ADCC00DC41FB /* NIKApiInvoker.swift */, 164 | 82D0A10E1C39B9BA00DC41FB /* NIKFile.swift */, 165 | ); 166 | path = API; 167 | sourceTree = ""; 168 | }; 169 | /* End PBXGroup section */ 170 | 171 | /* Begin PBXNativeTarget section */ 172 | 82D0A0EF1C39986000DC41FB /* SampleAppSwift */ = { 173 | isa = PBXNativeTarget; 174 | buildConfigurationList = 82D0A1021C39986000DC41FB /* Build configuration list for PBXNativeTarget "SampleAppSwift" */; 175 | buildPhases = ( 176 | 82D0A0EC1C39986000DC41FB /* Sources */, 177 | 82D0A0ED1C39986000DC41FB /* Frameworks */, 178 | 82D0A0EE1C39986000DC41FB /* Resources */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | ); 184 | name = SampleAppSwift; 185 | productName = SampleAppSwift; 186 | productReference = 82D0A0F01C39986000DC41FB /* SampleAppSwift.app */; 187 | productType = "com.apple.product-type.application"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | 82D0A0E81C39986000DC41FB /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | LastSwiftUpdateCheck = 0720; 196 | LastUpgradeCheck = 0720; 197 | ORGANIZATIONNAME = dreamfactory; 198 | TargetAttributes = { 199 | 82D0A0EF1C39986000DC41FB = { 200 | CreatedOnToolsVersion = 7.2; 201 | }; 202 | }; 203 | }; 204 | buildConfigurationList = 82D0A0EB1C39986000DC41FB /* Build configuration list for PBXProject "SampleAppSwift" */; 205 | compatibilityVersion = "Xcode 3.2"; 206 | developmentRegion = English; 207 | hasScannedForEncodings = 0; 208 | knownRegions = ( 209 | en, 210 | Base, 211 | ); 212 | mainGroup = 82D0A0E71C39986000DC41FB; 213 | productRefGroup = 82D0A0F11C39986000DC41FB /* Products */; 214 | projectDirPath = ""; 215 | projectRoot = ""; 216 | targets = ( 217 | 82D0A0EF1C39986000DC41FB /* SampleAppSwift */, 218 | ); 219 | }; 220 | /* End PBXProject section */ 221 | 222 | /* Begin PBXResourcesBuildPhase section */ 223 | 82D0A0EE1C39986000DC41FB /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 82D0A1071C3998E800DC41FB /* Main.storyboard in Resources */, 228 | 82D0A0FE1C39986000DC41FB /* LaunchScreen.storyboard in Resources */, 229 | 82CABEAC1C49493800AE8E85 /* PickerSelector.xib in Resources */, 230 | 82D0A0FB1C39986000DC41FB /* Assets.xcassets in Resources */, 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | /* End PBXResourcesBuildPhase section */ 235 | 236 | /* Begin PBXSourcesBuildPhase section */ 237 | 82D0A0EC1C39986000DC41FB /* Sources */ = { 238 | isa = PBXSourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | 82CABEAF1C495F6D00AE8E85 /* UIUtils.swift in Sources */, 242 | 824512211C3B76A6000EF52E /* GroupAddViewController.swift in Sources */, 243 | 824512251C3BCC6E000EF52E /* ContactViewController.swift in Sources */, 244 | 824512291C3BF6E4000EF52E /* ProfileImagePickerViewController.swift in Sources */, 245 | 82B3A0031C3AA70D00D4F23F /* AddressBookViewController.swift in Sources */, 246 | 82D0A1111C39C9B600DC41FB /* GroupRecord.swift in Sources */, 247 | 82B3A0011C3AA6EE00D4F23F /* RegisterViewController.swift in Sources */, 248 | 82D0A10D1C39ADCC00DC41FB /* NIKApiInvoker.swift in Sources */, 249 | 82D0A0F41C39986000DC41FB /* AppDelegate.swift in Sources */, 250 | 824512231C3B76C9000EF52E /* ContactListViewController.swift in Sources */, 251 | 824512271C3BF40A000EF52E /* ContactEditViewController.swift in Sources */, 252 | 82B39FFD1C3A9B7300D4F23F /* ContactInfoView.swift in Sources */, 253 | 82D0A10F1C39B9BA00DC41FB /* NIKFile.swift in Sources */, 254 | 82CABEAA1C49490400AE8E85 /* PickerSelector.swift in Sources */, 255 | 82B39FFF1C3AA42E00D4F23F /* MasterViewController.swift in Sources */, 256 | 82C7AEDF1C403D5C00E6909F /* RESTEngine.swift in Sources */, 257 | 82D0A1151C39CA2100DC41FB /* ContactDetailRecord.swift in Sources */, 258 | 82D0A1131C39CA0F00DC41FB /* ContactRecord.swift in Sources */, 259 | 82B39FF81C3A5B4600D4F23F /* CustomNavBar.swift in Sources */, 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXSourcesBuildPhase section */ 264 | 265 | /* Begin PBXVariantGroup section */ 266 | 82D0A0FC1C39986000DC41FB /* LaunchScreen.storyboard */ = { 267 | isa = PBXVariantGroup; 268 | children = ( 269 | 82D0A0FD1C39986000DC41FB /* Base */, 270 | ); 271 | name = LaunchScreen.storyboard; 272 | sourceTree = ""; 273 | }; 274 | 82D0A1051C3998E800DC41FB /* Main.storyboard */ = { 275 | isa = PBXVariantGroup; 276 | children = ( 277 | 82D0A1061C3998E800DC41FB /* Base */, 278 | ); 279 | name = Main.storyboard; 280 | sourceTree = ""; 281 | }; 282 | /* End PBXVariantGroup section */ 283 | 284 | /* Begin XCBuildConfiguration section */ 285 | 82D0A1001C39986000DC41FB /* Debug */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ALWAYS_SEARCH_USER_PATHS = NO; 289 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 290 | CLANG_CXX_LIBRARY = "libc++"; 291 | CLANG_ENABLE_MODULES = YES; 292 | CLANG_ENABLE_OBJC_ARC = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_CONSTANT_CONVERSION = YES; 295 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 296 | CLANG_WARN_EMPTY_BODY = YES; 297 | CLANG_WARN_ENUM_CONVERSION = YES; 298 | CLANG_WARN_INT_CONVERSION = YES; 299 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 300 | CLANG_WARN_UNREACHABLE_CODE = YES; 301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 302 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 303 | COPY_PHASE_STRIP = NO; 304 | DEBUG_INFORMATION_FORMAT = dwarf; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | ENABLE_TESTABILITY = YES; 307 | GCC_C_LANGUAGE_STANDARD = gnu99; 308 | GCC_DYNAMIC_NO_PIC = NO; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | GCC_OPTIMIZATION_LEVEL = 0; 311 | GCC_PREPROCESSOR_DEFINITIONS = ( 312 | "DEBUG=1", 313 | "$(inherited)", 314 | ); 315 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 316 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 317 | GCC_WARN_UNDECLARED_SELECTOR = YES; 318 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 319 | GCC_WARN_UNUSED_FUNCTION = YES; 320 | GCC_WARN_UNUSED_VARIABLE = YES; 321 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 322 | MTL_ENABLE_DEBUG_INFO = YES; 323 | ONLY_ACTIVE_ARCH = YES; 324 | SDKROOT = iphoneos; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | }; 327 | name = Debug; 328 | }; 329 | 82D0A1011C39986000DC41FB /* Release */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 334 | CLANG_CXX_LIBRARY = "libc++"; 335 | CLANG_ENABLE_MODULES = YES; 336 | CLANG_ENABLE_OBJC_ARC = YES; 337 | CLANG_WARN_BOOL_CONVERSION = YES; 338 | CLANG_WARN_CONSTANT_CONVERSION = YES; 339 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 340 | CLANG_WARN_EMPTY_BODY = YES; 341 | CLANG_WARN_ENUM_CONVERSION = YES; 342 | CLANG_WARN_INT_CONVERSION = YES; 343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 344 | CLANG_WARN_UNREACHABLE_CODE = YES; 345 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 346 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 347 | COPY_PHASE_STRIP = NO; 348 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 349 | ENABLE_NS_ASSERTIONS = NO; 350 | ENABLE_STRICT_OBJC_MSGSEND = YES; 351 | GCC_C_LANGUAGE_STANDARD = gnu99; 352 | GCC_NO_COMMON_BLOCKS = YES; 353 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 354 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 355 | GCC_WARN_UNDECLARED_SELECTOR = YES; 356 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 357 | GCC_WARN_UNUSED_FUNCTION = YES; 358 | GCC_WARN_UNUSED_VARIABLE = YES; 359 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 360 | MTL_ENABLE_DEBUG_INFO = NO; 361 | SDKROOT = iphoneos; 362 | VALIDATE_PRODUCT = YES; 363 | }; 364 | name = Release; 365 | }; 366 | 82D0A1031C39986000DC41FB /* Debug */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 370 | INFOPLIST_FILE = SampleAppSwift/Info.plist; 371 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 372 | PRODUCT_BUNDLE_IDENTIFIER = com.dreamfactory.SampleAppSwift; 373 | PRODUCT_NAME = "$(TARGET_NAME)"; 374 | }; 375 | name = Debug; 376 | }; 377 | 82D0A1041C39986000DC41FB /* Release */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 381 | INFOPLIST_FILE = SampleAppSwift/Info.plist; 382 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 383 | PRODUCT_BUNDLE_IDENTIFIER = com.dreamfactory.SampleAppSwift; 384 | PRODUCT_NAME = "$(TARGET_NAME)"; 385 | }; 386 | name = Release; 387 | }; 388 | /* End XCBuildConfiguration section */ 389 | 390 | /* Begin XCConfigurationList section */ 391 | 82D0A0EB1C39986000DC41FB /* Build configuration list for PBXProject "SampleAppSwift" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | 82D0A1001C39986000DC41FB /* Debug */, 395 | 82D0A1011C39986000DC41FB /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | 82D0A1021C39986000DC41FB /* Build configuration list for PBXNativeTarget "SampleAppSwift" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | 82D0A1031C39986000DC41FB /* Debug */, 404 | 82D0A1041C39986000DC41FB /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | /* End XCConfigurationList section */ 410 | }; 411 | rootObject = 82D0A0E81C39986000DC41FB /* Project object */; 412 | } 413 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift.xcodeproj/project.xcworkspace/xcuserdata/tigr.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift.xcodeproj/project.xcworkspace/xcuserdata/tigr.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift.xcodeproj/xcuserdata/tigr.xcuserdatad/xcschemes/SampleAppSwift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift.xcodeproj/xcuserdata/tigr.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SampleAppSwift.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 82D0A0EF1C39986000DC41FB 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | var globalToolBar: CustomNavBar! 16 | 17 | 18 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 19 | // Override point for customization after application launch. 20 | 21 | // Build persistent navigation bar 22 | let tb = CustomNavBar() 23 | tb.barStyle = .Default 24 | tb.sizeToFit() 25 | tb.frame = CGRectMake(tb.frame.origin.x, tb.frame.origin.y, tb.frame.size.width, 67) 26 | 27 | tb.buildLogo() 28 | tb.buildButtons() 29 | tb.reloadInputViews() 30 | 31 | window?.rootViewController?.view.addSubview(tb) 32 | globalToolBar = tb 33 | 34 | return true 35 | } 36 | 37 | func applicationWillResignActive(application: UIApplication) { 38 | // 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. 39 | // 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. 40 | } 41 | 42 | func applicationDidEnterBackground(application: UIApplication) { 43 | // 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. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | func applicationWillEnterForeground(application: UIApplication) { 48 | // 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. 49 | } 50 | 51 | func applicationDidBecomeActive(application: UIApplication) { 52 | // 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. 53 | } 54 | 55 | func applicationWillTerminate(application: UIApplication) { 56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 57 | } 58 | } 59 | 60 | extension UIViewController { 61 | var navBar: CustomNavBar { 62 | let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate 63 | return appDelegate.globalToolBar 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/DreamFactory-logo-horiz-filled.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "DreamFactory-logo-horiz-filled.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/DreamFactory-logo-horiz-filled.imageset/DreamFactory-logo-horiz-filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/DreamFactory-logo-horiz-filled.imageset/DreamFactory-logo-horiz-filled.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/DreamFactory-logo-horiz.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "DreamFactory-logo-horiz.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/DreamFactory-logo-horiz.imageset/DreamFactory-logo-horiz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/DreamFactory-logo-horiz.imageset/DreamFactory-logo-horiz.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/chat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "chat.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/chat.imageset/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/chat.imageset/chat.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/default_portrait.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "default_portrait.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/default_portrait.imageset/default_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/default_portrait.imageset/default_portrait.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "home.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/home.imageset/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/home.imageset/home.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/mail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "mail.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/mail.imageset/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/mail.imageset/mail.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/phone1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "phone1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/phone1.imageset/phone1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/phone1.imageset/phone1.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/skype.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "skype.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/skype.imageset/skype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/skype.imageset/skype.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/twitter2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "twitter2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Assets.xcassets/twitter2.imageset/twitter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/SampleAppSwift/Assets.xcassets/twitter2.imageset/twitter2.png -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/AddressBookViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressBookViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AddressBookViewController: UITableViewController { 12 | // list of groups 13 | var addressBookContentArray: [GroupRecord] = [] 14 | 15 | // for prefetching data 16 | var contactListViewController: ContactListViewController! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | } 21 | 22 | override func viewWillAppear(animated: Bool) { 23 | super.viewWillAppear(animated) 24 | 25 | let navBar = self.navBar 26 | navBar.showAdd() 27 | navBar.showBackButton(true) 28 | navBar.addButton.addTarget(self, action: #selector(hitAddGroupButton), forControlEvents: .TouchDown) 29 | navBar.enableAllTouch() 30 | 31 | getAddressBookContentFromServer() 32 | } 33 | 34 | override func viewWillDisappear(animated: Bool) { 35 | super.viewWillDisappear(animated) 36 | 37 | self.navBar.addButton.removeTarget(self, action: #selector(hitAddGroupButton), forControlEvents: .TouchDown) 38 | } 39 | 40 | func hitAddGroupButton() { 41 | showGroupAddViewController() 42 | } 43 | 44 | //MARK: - Table view data source 45 | 46 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return addressBookContentArray.count 48 | } 49 | 50 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCellWithIdentifier("addressBookTableViewCell", forIndexPath: indexPath) 52 | 53 | let record = addressBookContentArray[indexPath.row] 54 | cell.textLabel?.text = record.name 55 | 56 | return cell 57 | } 58 | 59 | //MARK: - Table view delegate 60 | 61 | override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { 62 | // allow swipe to delete 63 | return true 64 | } 65 | 66 | override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { 67 | if editingStyle == .Delete { 68 | let record = addressBookContentArray[indexPath.row] 69 | 70 | // can not delete group until all references to it are removed 71 | // remove relations -> remove group 72 | // pass record ID so it knows what group we are removing 73 | removeGroupFromServer(record.id) 74 | 75 | addressBookContentArray.removeAtIndex(indexPath.row) 76 | tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) 77 | } 78 | } 79 | 80 | override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) { 81 | let record = addressBookContentArray[indexPath.row] 82 | 83 | contactListViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContactListViewController") as! ContactListViewController 84 | contactListViewController.groupRecord = record 85 | contactListViewController.prefetch() 86 | } 87 | 88 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 89 | tableView.deselectRowAtIndexPath(indexPath, animated: true) 90 | showContactListViewController() 91 | } 92 | 93 | //MARK: - Private functions 94 | 95 | private func getAddressBookContentFromServer() { 96 | // get all the groups 97 | RESTEngine.sharedEngine.getAddressBookContentFromServerWithSuccess({ response in 98 | self.addressBookContentArray.removeAll() 99 | let records = response!["resource"] as! JSONArray 100 | for recordInfo in records { 101 | let newRecord = GroupRecord(json: recordInfo) 102 | self.addressBookContentArray.append(newRecord) 103 | } 104 | dispatch_async(dispatch_get_main_queue()) { 105 | self.tableView.reloadData() 106 | } 107 | }, failure: { error in 108 | NSLog("Error getting address book data: \(error)") 109 | dispatch_async(dispatch_get_main_queue()) { 110 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 111 | self.navigationController?.popToRootViewControllerAnimated(true) 112 | } 113 | }) 114 | } 115 | 116 | private func removeGroupFromServer(groupId: NSNumber) { 117 | RESTEngine.sharedEngine.removeGroupFromServerWithGroupId(groupId, success: nil, failure: { error in 118 | NSLog("Error deleting group: \(error)") 119 | dispatch_async(dispatch_get_main_queue()) { 120 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 121 | self.navigationController?.popToRootViewControllerAnimated(true) 122 | } 123 | }) 124 | } 125 | 126 | private func showContactListViewController() { 127 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 128 | // already fetching so just wait until the data gets back 129 | self.contactListViewController.waitToReady() 130 | dispatch_async(dispatch_get_main_queue()) { 131 | self.navigationController?.pushViewController(self.contactListViewController, animated: true) 132 | } 133 | } 134 | } 135 | 136 | private func showGroupAddViewController() { 137 | let groupAddViewController = self.storyboard?.instantiateViewControllerWithIdentifier("GroupAddViewController") as! GroupAddViewController 138 | // tell the viewController we are creating a new group 139 | groupAddViewController.prefetch() 140 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 141 | // already fetching so just wait until the data gets back 142 | groupAddViewController.waitToReady() 143 | dispatch_async(dispatch_get_main_queue()) { 144 | self.navigationController?.pushViewController(groupAddViewController, animated: true) 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/ContactEditViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactEditViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/5/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ContactEditViewController: UIViewController, ProfileImagePickerDelegate, UITextFieldDelegate, ContactInfoDelegate, PickerSelectorDelegate { 12 | @IBOutlet weak var contactEditScrollView: UIScrollView! 13 | 14 | weak var contactViewController: ContactViewController? 15 | 16 | // the contact being looked at 17 | var contactRecord: ContactRecord? 18 | 19 | // set when editing an existing contact 20 | // list of contactinfo records 21 | var contactDetails: [ContactDetailRecord]? 22 | 23 | // set when creating a new contact 24 | // id of the group the contact is being created in 25 | var contactGroupId: NSNumber! 26 | 27 | // all the text fields we programmatically create 28 | private var textFields: [String: UITextField] = [:] 29 | 30 | // holds all new contact info fields 31 | private var addedContactInfo: [ContactDetailRecord] = [] 32 | 33 | // for handling a profile image set up for a new user 34 | private var imageURL = "" 35 | private var profileImage: UIImage? 36 | 37 | private weak var selectedContactInfoView: ContactInfoView! 38 | private weak var addButtonRef: UIButton! // reference to bottom AddButton 39 | private weak var activeTextField: UITextField? 40 | private var contactInfoViewHeight: CGFloat = 0 // stores contact view height 41 | 42 | deinit { 43 | NSNotificationCenter.defaultCenter().removeObserver(self) 44 | } 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | 49 | contactEditScrollView.frame = CGRectMake(0, 0, view.frame.size.width, view.frame.size.height) 50 | contactEditScrollView.backgroundColor = UIColor(red: 245/255.0, green: 245/255.0, blue: 245/255.0, alpha: 1.0) 51 | 52 | buildContactFields() 53 | 54 | // resize scrollview 55 | var contentRect = CGRectZero 56 | for view in contactEditScrollView.subviews { 57 | contentRect = CGRectUnion(contentRect, view.frame) 58 | } 59 | 60 | contactEditScrollView.contentSize = contentRect.size 61 | registerForKeyboardNotifications() 62 | } 63 | 64 | override func viewWillAppear(animated: Bool) { 65 | super.viewWillAppear(animated) 66 | 67 | 68 | let navBar = self.navBar 69 | navBar.showDone() 70 | navBar.doneButton.addTarget(self, action: #selector(onDoneButtonClick), forControlEvents: .TouchDown) 71 | navBar.enableAllTouch() 72 | } 73 | 74 | override func viewWillDisappear(animated: Bool) { 75 | super.viewWillDisappear(animated) 76 | 77 | self.navBar.doneButton.removeTarget(self, action: #selector(onDoneButtonClick), forControlEvents: .TouchDown) 78 | } 79 | 80 | private func registerForKeyboardNotifications() { 81 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWasShown(_:)), name: UIKeyboardDidShowNotification, object: nil) 82 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillBeHidden(_:)), name: UIKeyboardWillHideNotification, object: nil) 83 | } 84 | 85 | func keyboardWasShown(notification: NSNotification) { 86 | if activeTextField == nil { 87 | return 88 | } 89 | 90 | let info = notification.userInfo 91 | let kbSize = info![UIKeyboardFrameBeginUserInfoKey]!.CGRectValue.size 92 | 93 | let contentInsets = UIEdgeInsetsMake(contactEditScrollView.contentInset.top, 0, kbSize.height, 0) 94 | contactEditScrollView.contentInset = contentInsets 95 | contactEditScrollView.scrollIndicatorInsets = contentInsets 96 | 97 | var aRect = view.frame 98 | aRect.size.height -= kbSize.height 99 | if !CGRectContainsPoint(aRect, activeTextField!.frame.origin) { 100 | contactEditScrollView.scrollRectToVisible(activeTextField!.frame, animated: true) 101 | } 102 | } 103 | 104 | func keyboardWillBeHidden(notification: NSNotification) { 105 | let contentInsets = UIEdgeInsetsMake(contactEditScrollView.contentInset.top, 0, 0, 0) 106 | contactEditScrollView.contentInset = contentInsets 107 | contactEditScrollView.scrollIndicatorInsets = contentInsets 108 | } 109 | 110 | func onDoneButtonClick() { 111 | let firstNameOptional = textFields["First Name"]?.text 112 | let lastNameOptional = textFields["Last Name"]?.text 113 | 114 | guard let firstName = firstNameOptional where !firstName.isEmpty, 115 | let lastName = lastNameOptional where !lastName.isEmpty 116 | else { 117 | let alert = UIAlertController(title: nil, message: "Please enter a first and last name for the contact", preferredStyle: .Alert) 118 | alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) 119 | presentViewController(alert, animated: true, completion: nil) 120 | return 121 | } 122 | 123 | self.navBar.disableAllTouch() 124 | 125 | if contactRecord != nil { 126 | //updating existing contact 127 | if !imageURL.isEmpty && profileImage != nil { 128 | putLocalImageOnServer(profileImage!) 129 | } else { 130 | updateContactWithServer() 131 | } 132 | } else { 133 | // need to create the contact before creating addresses or adding 134 | // the contact to any groups 135 | addContactToServer() 136 | } 137 | } 138 | 139 | func onAddNewAddressClick() { 140 | // make room for a new view and insert it 141 | let height: CGFloat = max(contactInfoViewHeight, 345.0) 142 | let y = addButtonRef.frame.origin.y 143 | var translation = CGRectZero 144 | 145 | translation.origin.y = y + height + 30 146 | 147 | UIView.beginAnimations(nil, context: nil) 148 | UIView.setAnimationDuration(0.25) 149 | 150 | // move the button down 151 | addButtonRef.center = CGPointMake(contactEditScrollView.frame.size.width * 0.5, translation.origin.y + addButtonRef.frame.size.height * 0.5) 152 | 153 | // make the view scroll down too 154 | var contentRect = contactEditScrollView.contentSize 155 | contentRect.height = addButtonRef.frame.origin.y + addButtonRef.frame.size.height 156 | contactEditScrollView.contentSize = contentRect 157 | 158 | let bottomOffset = CGPointMake(0, translation.origin.y + addButtonRef.frame.size.height - contactEditScrollView.frame.size.height) 159 | contactEditScrollView.setContentOffset(bottomOffset, animated: true) 160 | 161 | UIView.commitAnimations() 162 | 163 | // build new view 164 | let contactInfoView = ContactInfoView(frame: CGRectMake(0, y, contactEditScrollView.frame.size.width, 0)) 165 | contactInfoView.delegate = self 166 | contactInfoView.setTextFieldsDelegate(self) 167 | 168 | let record = ContactDetailRecord() 169 | addedContactInfo.append(record) 170 | contactEditScrollView.addSubview(contactInfoView) 171 | contactInfoView.record = record 172 | } 173 | 174 | func onChangeImageClick() { 175 | let profileImagePickerViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ProfileImagePickerViewController") as! ProfileImagePickerViewController 176 | profileImagePickerViewController.delegate = self 177 | profileImagePickerViewController.record = contactRecord! 178 | 179 | self.navigationController?.pushViewController(profileImagePickerViewController, animated: true) 180 | } 181 | 182 | // Profile image picker delegate 183 | 184 | func didSelectItem(item: String) { 185 | self.navigationController?.popViewControllerAnimated(true) 186 | // gets info passed back up from the image picker 187 | self.contactRecord!.imageURL = item 188 | } 189 | 190 | func didSelectItem(item: String, withImage image: UIImage) { 191 | self.navigationController?.popViewControllerAnimated(true) 192 | // gets info passed back up from the image picker 193 | self.imageURL = item 194 | self.profileImage = image 195 | } 196 | 197 | // MARK: - Text field delegate 198 | 199 | func textFieldShouldReturn(textField: UITextField) -> Bool { 200 | textField.resignFirstResponder() 201 | return true 202 | } 203 | 204 | func textFieldDidBeginEditing(textField: UITextField) { 205 | activeTextField = textField 206 | } 207 | 208 | func textFieldDidEndEditing(textField: UITextField) { 209 | activeTextField = nil 210 | } 211 | 212 | // MARK: - ContactInfo delegate 213 | func onContactTypeClick(view: ContactInfoView, withTypes types: [String]) { 214 | let picker = PickerSelector() 215 | picker.pickerData = types 216 | picker.delegate = self 217 | picker.showPickerOver(self) 218 | selectedContactInfoView = view 219 | } 220 | 221 | // MARK: - Picker delegate 222 | 223 | func pickerSelector(selector: PickerSelector, selectedValue value: String, index: Int) { 224 | selectedContactInfoView.contactType = value 225 | } 226 | 227 | // MARK: - Private methods 228 | 229 | private func putValueIn(value: String, forKey key: String) { 230 | if !value.isEmpty { 231 | textFields[key]?.text = value 232 | } 233 | } 234 | 235 | // build ui programmatically 236 | private func buildContactFields() { 237 | buildContactTextFields("Contact Details", names: ["First Name", "Last Name", "Twitter", "Skype", "Notes"]) 238 | 239 | // populate contact fields if editing 240 | if let contactRecord = contactRecord { 241 | putValueIn(contactRecord.firstName, forKey: "First Name") 242 | putValueIn(contactRecord.lastName, forKey: "Last Name") 243 | putValueIn(contactRecord.twitter, forKey: "Twitter") 244 | putValueIn(contactRecord.skype, forKey: "Skype") 245 | putValueIn(contactRecord.notes, forKey: "Notes") 246 | } 247 | 248 | let changeImageButton = UIButton(type: .System) 249 | var y = CGRectGetMaxY(contactEditScrollView.subviews.last!.frame) 250 | changeImageButton.frame = CGRectMake(0, y + 10, view.frame.size.width, 40) 251 | changeImageButton.titleLabel?.textAlignment = .Center 252 | changeImageButton.setTitle("Change image", forState: .Normal) 253 | changeImageButton.titleLabel?.font = UIFont(name: "Helvetica Neue", size: 20.0) 254 | changeImageButton.setTitleColor(UIColor(red: 107/255.0, green: 170/255.0, blue: 178/255.0, alpha: 1.0), forState: .Normal) 255 | 256 | changeImageButton.addTarget(self, action: #selector(onChangeImageClick), forControlEvents: .TouchUpInside) 257 | //contactEditScrollView.addSubview(changeImageButton) 258 | 259 | // add all the contact info views 260 | if let contactDetails = contactDetails { 261 | 262 | for record in contactDetails { 263 | let y = CGRectGetMaxY(contactEditScrollView.subviews.last!.frame) 264 | let contactInfoView = ContactInfoView(frame: CGRectMake(0, y, view.frame.size.width, 40)) 265 | contactInfoView.delegate = self 266 | contactInfoView.setTextFieldsDelegate(self) 267 | 268 | contactInfoView.record = record 269 | contactInfoView.updateFields() 270 | 271 | contactEditScrollView.addSubview(contactInfoView) 272 | contactInfoViewHeight = contactInfoView.frame.size.height 273 | } 274 | } 275 | 276 | // create button to add a new address 277 | y = CGRectGetMaxY(contactEditScrollView.subviews.last!.frame) 278 | let addButton = UIButton(type: .System) 279 | addButton.frame = CGRectMake(0, y + 10, view.frame.size.width, 40) 280 | addButton.backgroundColor = UIColor(red: 107/255.0, green: 170/255.0, blue: 178/255.0, alpha: 1.0) 281 | 282 | addButton.titleLabel?.textAlignment = .Center 283 | addButton.titleLabel?.font = UIFont(name: "Helvetica Neue", size: 20.0) 284 | addButton.setTitleColor(UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0), forState: .Normal) 285 | addButton.setTitle("Add new address", forState: .Normal) 286 | addButton.addTarget(self, action: #selector(onAddNewAddressClick), forControlEvents: .TouchUpInside) 287 | 288 | contactEditScrollView.addSubview(addButton) 289 | addButtonRef = addButton 290 | } 291 | 292 | private func buildContactTextFields(title: String, names: [String]) { 293 | var y: CGFloat = 30 294 | for field in names { 295 | let textField = UITextField(frame: CGRectMake(view.frame.size.width * 0.05, y, view.frame.size.width*0.9, 35)) 296 | textField.placeholder = field 297 | textField.font = UIFont(name: "Helvetica Neue", size: 20.0) 298 | textField.backgroundColor = UIColor.whiteColor() 299 | textField.layer.cornerRadius = 5 300 | textField.delegate = self; 301 | contactEditScrollView.addSubview(textField) 302 | textFields[field] = textField 303 | 304 | y += 40 305 | } 306 | } 307 | 308 | private func addContactToServer() { 309 | // set up the contact image filename 310 | var fileName = "" 311 | if !imageURL.isEmpty { 312 | fileName = "\(imageURL).jpg" 313 | } 314 | 315 | let requestBody: [String: AnyObject] = ["first_name": textFields["First Name"]!.text!, 316 | "last_name": textFields["Last Name"]!.text!, 317 | "filename": fileName, 318 | "notes": textFields["Notes"]!.text!, 319 | "twitter": textFields["Twitter"]!.text!, 320 | "skype": textFields["Skype"]!.text!] 321 | 322 | // build the contact and fill it so we don't have to reload when we go up a level 323 | contactRecord = ContactRecord() 324 | 325 | RESTEngine.sharedEngine.addContactToServerWithDetails(requestBody, success: { response in 326 | let records = response!["resource"] as! JSONArray 327 | for recordInfo in records { 328 | self.contactRecord!.id = (recordInfo["id"] as! NSNumber) 329 | } 330 | if !self.imageURL.isEmpty && self.profileImage != nil { 331 | self.createProfileImageOnServer() 332 | } else { 333 | self.addContactGroupRelationToServer() 334 | } 335 | }, failure: { error in 336 | NSLog("Error adding new contact to server: \(error)") 337 | dispatch_async(dispatch_get_main_queue()) { 338 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 339 | self.navBar.enableAllTouch() 340 | } 341 | }) 342 | } 343 | 344 | private func addContactGroupRelationToServer() { 345 | RESTEngine.sharedEngine.addContactGroupRelationToServerWithContactId(contactRecord!.id, groupId: contactGroupId, success: { _ in 346 | self.addContactInfoToServer() 347 | 348 | }, failure: { error in 349 | NSLog("Error adding contact group relation to server from contact edit: \(error)") 350 | dispatch_async(dispatch_get_main_queue()) { 351 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 352 | self.navigationController?.popToRootViewControllerAnimated(true) 353 | } 354 | }) 355 | } 356 | 357 | private func addContactInfoToServer() { 358 | // build request body 359 | var records: JSONArray = [] 360 | /* 361 | * Format is: 362 | * { 363 | * "resource":[ 364 | * {...}, 365 | * {...} 366 | * ] 367 | * } 368 | * 369 | */ 370 | 371 | // fill body with contact details 372 | for view in contactEditScrollView.subviews { 373 | if let view = view as? ContactInfoView { 374 | if view.record?.id == nil || view.record?.id == 0 { 375 | view.record.id = NSNumber(integer: 0) 376 | view.record.contactId = contactRecord!.id 377 | 378 | var shouldBreak = false 379 | view.validateInfoWithResult { success, message in 380 | shouldBreak = !success 381 | if !success { 382 | dispatch_async(dispatch_get_main_queue()) { 383 | Alert.showAlertWithMessage(message!, fromViewController: self) 384 | self.navBar.enableAllTouch() 385 | } 386 | } 387 | } 388 | if shouldBreak { 389 | return 390 | } 391 | 392 | view.updateRecord() 393 | records.append(view.buildToDiciontary()) 394 | contactDetails?.append(view.record) 395 | } 396 | } 397 | } 398 | 399 | // make sure we don't try to put contact info up on the server if we don't have any 400 | // need to check down here because of the way they are set up 401 | if records.isEmpty { 402 | dispatch_async(dispatch_get_main_queue()) { 403 | self.waitToGoBack() 404 | } 405 | return 406 | } 407 | 408 | RESTEngine.sharedEngine.addContactInfoToServer(records, success: { _ in 409 | // head back up only once all the data has been loaded 410 | dispatch_async(dispatch_get_main_queue()) { 411 | self.waitToGoBack() 412 | } 413 | }, failure: { error in 414 | NSLog("Error putting contact details back up on server: \(error)") 415 | dispatch_async(dispatch_get_main_queue()) { 416 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 417 | self.navBar.enableAllTouch() 418 | } 419 | }) 420 | } 421 | 422 | private func createProfileImageOnServer() { 423 | var fileName = "UserFile1.jpg" // default file name 424 | if !imageURL.isEmpty { 425 | fileName = "\(imageURL).jpg" 426 | } 427 | 428 | RESTEngine.sharedEngine.addContactImageWithContactId(contactRecord!.id, image: self.profileImage!, imageName: fileName, success: { _in in 429 | self.addContactGroupRelationToServer() 430 | }, failure: { error in 431 | NSLog("Error creating new profile image folder on server: \(error)") 432 | dispatch_async(dispatch_get_main_queue()) { 433 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 434 | self.navBar.enableAllTouch() 435 | } 436 | }) 437 | } 438 | 439 | private func putLocalImageOnServer(image: UIImage) { 440 | var fileName = "UserFile1.jpg" // default file name 441 | if !imageURL.isEmpty { 442 | fileName = "\(imageURL).jpg" 443 | } 444 | 445 | RESTEngine.sharedEngine.putImageToFolderWithPath("\(contactRecord!.id)", image: image, fileName: fileName, success: { _ in 446 | self.contactRecord?.imageURL = fileName 447 | self.updateContactWithServer() 448 | 449 | }, failure: { error in 450 | NSLog("Error creating image on server: \(error)") 451 | dispatch_async(dispatch_get_main_queue()) { 452 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 453 | self.navBar.enableAllTouch() 454 | } 455 | }) 456 | } 457 | 458 | private func updateContactWithServer() { 459 | let requestBody: [String: AnyObject] = ["first_name": textFields["First Name"]!.text!, 460 | "last_name": textFields["Last Name"]!.text!, 461 | "notes": textFields["Notes"]!.text!, 462 | "twitter": textFields["Twitter"]!.text!, 463 | "skype": textFields["Skype"]!.text!] 464 | 465 | // update the contact 466 | contactRecord!.firstName = requestBody["first_name"] as! String 467 | contactRecord!.lastName = requestBody["last_name"] as! String 468 | contactRecord!.notes = requestBody["notes"] as! String 469 | contactRecord!.twitter = requestBody["twitter"] as! String 470 | contactRecord!.skype = requestBody["skype"] as! String 471 | 472 | RESTEngine.sharedEngine.updateContactWithContactId(contactRecord!.id, contactDetails: requestBody, success: { _ in 473 | self.updateContactInfoWithServer() 474 | }, failure: { error in 475 | NSLog("Error updating contact info with server: \(error)") 476 | dispatch_async(dispatch_get_main_queue()) { 477 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 478 | self.navBar.enableAllTouch() 479 | } 480 | }) 481 | } 482 | 483 | private func updateContactInfoWithServer() { 484 | // build request body 485 | var records: JSONArray = [] 486 | 487 | for view in contactEditScrollView.subviews { 488 | if let view = view as? ContactInfoView { 489 | if view.record.contactId != nil { 490 | 491 | var shouldBreak = false 492 | view.validateInfoWithResult { success, message in 493 | shouldBreak = !success 494 | if !success { 495 | dispatch_async(dispatch_get_main_queue()) { 496 | Alert.showAlertWithMessage(message!, fromViewController: self) 497 | self.navBar.enableAllTouch() 498 | } 499 | } 500 | } 501 | if shouldBreak { 502 | return 503 | } 504 | 505 | view.updateRecord() 506 | if view.record.id != 0 { 507 | records.append(view.buildToDiciontary()) 508 | } 509 | } 510 | } 511 | } 512 | 513 | if records.isEmpty { 514 | // if we have no records to update, check if we have any records to add 515 | addContactInfoToServer() 516 | return 517 | } 518 | 519 | RESTEngine.sharedEngine.updateContactInfo(records, success: { _ in 520 | self.addContactInfoToServer() 521 | }, failure: { error in 522 | NSLog("Error updating contact details on server: \(error)") 523 | dispatch_async(dispatch_get_main_queue()) { 524 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 525 | self.navBar.enableAllTouch() 526 | } 527 | }) 528 | } 529 | 530 | private func waitToGoBack() { 531 | if let contactViewController = contactViewController { 532 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 533 | contactViewController.prefetch() 534 | contactViewController.waitToReady() 535 | self.contactViewController = nil 536 | dispatch_async(dispatch_get_main_queue()) { 537 | self.navigationController?.popViewControllerAnimated(true) 538 | } 539 | } 540 | } else { 541 | self.navigationController?.popViewControllerAnimated(true) 542 | } 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/ContactListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactListViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/5/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ContactListViewController: UITableViewController, UISearchBarDelegate { 12 | // which group is being viewed 13 | var groupRecord: GroupRecord! 14 | 15 | private var searchBar: UISearchBar! 16 | 17 | // if there is a search going on 18 | private var isSearch = false 19 | 20 | // holds contents of a search 21 | private var displayContentArray: [ContactRecord] = [] 22 | 23 | // contacts broken into groups by first letter of last name 24 | private var contactSectionsDictionary: [String: [ContactRecord]]! 25 | 26 | // header letters 27 | private var alphabetArray: [String]! 28 | 29 | // for prefetching data 30 | private var contactViewController: ContactViewController? 31 | private var goingToShowContactViewController = false 32 | private var didPrefetch = false 33 | private var viewLock: NSCondition! 34 | private var viewReady = false 35 | private var queue: dispatch_queue_t! 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | // set up the search bar programmatically 41 | searchBar = UISearchBar(frame: CGRectMake(0, 0, view.frame.size.width, 44)) 42 | searchBar.delegate = self 43 | tableView.tableHeaderView = searchBar 44 | 45 | tableView.allowsMultipleSelectionDuringEditing = false 46 | } 47 | 48 | override func viewWillAppear(animated: Bool) { 49 | if !didPrefetch { 50 | dispatch_async(queue) {[weak self] in 51 | if let strongSelf = self { 52 | strongSelf.getContactsListFromServerWithRelation() 53 | } 54 | } 55 | } 56 | 57 | super.viewWillAppear(animated) 58 | 59 | contactViewController = nil 60 | goingToShowContactViewController = false 61 | // reload the view 62 | isSearch = false 63 | searchBar.text = "" 64 | didPrefetch = false 65 | 66 | let navBar = self.navBar 67 | navBar.addButton.addTarget(self, action: #selector(onAddButtonClick), forControlEvents: .TouchDown) 68 | navBar.editButton.addTarget(self, action: #selector(onEditButtonClick), forControlEvents: .TouchDown) 69 | navBar.showEditAndAdd() 70 | navBar.enableAllTouch() 71 | } 72 | 73 | override func viewWillDisappear(animated: Bool) { 74 | super.viewWillDisappear(animated) 75 | 76 | let navBar = self.navBar 77 | navBar.addButton.removeTarget(self, action: #selector(onAddButtonClick), forControlEvents: .TouchDown) 78 | navBar.editButton.removeTarget(self, action: #selector(onEditButtonClick), forControlEvents: .TouchDown) 79 | 80 | if !goingToShowContactViewController && contactViewController != nil { 81 | contactViewController!.cancelPrefetch() 82 | contactViewController = nil 83 | } 84 | } 85 | 86 | func onAddButtonClick() { 87 | showContactEditViewController() 88 | } 89 | 90 | func onEditButtonClick() { 91 | showGroupEditViewController() 92 | } 93 | 94 | func prefetch() { 95 | if viewLock == nil { 96 | viewLock = NSCondition() 97 | } 98 | viewLock.lock() 99 | viewReady = false 100 | didPrefetch = true 101 | 102 | if queue == nil { 103 | queue = dispatch_queue_create("contactListQueue", nil) 104 | } 105 | 106 | dispatch_async(queue) {[weak self] in 107 | if let strongSelf = self { 108 | strongSelf.getContactsListFromServerWithRelation() 109 | } 110 | } 111 | } 112 | 113 | // blocks until the data has been fetched 114 | func waitToReady() { 115 | viewLock.lock() 116 | while !viewReady { 117 | viewLock.wait() 118 | } 119 | viewLock.unlock() 120 | } 121 | 122 | // MARK: - Search bar delegate 123 | 124 | func searchBar(searchBar: UISearchBar, textDidChange searchText: String) { 125 | displayContentArray.removeAll() 126 | 127 | if searchText.isEmpty { 128 | // done with searching, show all the data 129 | isSearch = false 130 | tableView.reloadData() 131 | return 132 | } 133 | isSearch = true 134 | let firstLetter = searchText.substringToIndex(searchText.startIndex.advancedBy(1)).uppercaseString 135 | let arrayAtLetter = contactSectionsDictionary[firstLetter] 136 | if let arrayAtLetter = arrayAtLetter { 137 | for record in arrayAtLetter { 138 | if record.lastName.characters.count < searchText.characters.count { 139 | continue 140 | } 141 | let lastNameSubstring = record.lastName.substringToIndex(record.lastName.startIndex.advancedBy(searchText.characters.count)) 142 | if lastNameSubstring.caseInsensitiveCompare(searchText) == .OrderedSame { 143 | displayContentArray.append(record) 144 | } 145 | } 146 | tableView.reloadData() 147 | } 148 | } 149 | 150 | func searchBarSearchButtonClicked(searchBar: UISearchBar) { 151 | searchBar.resignFirstResponder() 152 | } 153 | 154 | func searchBarCancelButtonClicked(searchBar: UISearchBar) { 155 | searchBar.resignFirstResponder() 156 | } 157 | 158 | // MARK: - Table view data source 159 | 160 | override func numberOfSectionsInTableView(tableView: UITableView) -> Int { 161 | if isSearch { 162 | return 1 163 | } 164 | return alphabetArray.count 165 | } 166 | 167 | override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 168 | if isSearch { 169 | return displayContentArray.count 170 | } 171 | let sectionContacts = contactSectionsDictionary[alphabetArray[section]]! 172 | return sectionContacts.count 173 | } 174 | 175 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 176 | let cell = tableView.dequeueReusableCellWithIdentifier("contactListTableViewCell", forIndexPath: indexPath) 177 | 178 | let record = recordForIndexPath(indexPath) 179 | 180 | cell.textLabel?.text = record.fullName 181 | 182 | return cell 183 | } 184 | 185 | // MARK: - Table view delegate 186 | 187 | override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 188 | if isSearch { 189 | let searchText = searchBar.text 190 | if searchText?.characters.count > 0 { 191 | return searchText!.substringToIndex(searchText!.startIndex.advancedBy(1)).uppercaseString 192 | } 193 | } 194 | return alphabetArray[section] 195 | } 196 | 197 | override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { 198 | return true 199 | } 200 | 201 | override func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 202 | view.tintColor = UIColor(red: 210/255.0, green: 225/255.0, blue: 239/255.0, alpha: 1.0) 203 | } 204 | 205 | override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) { 206 | let record = recordForIndexPath(indexPath) 207 | 208 | if let contactViewController = contactViewController { 209 | contactViewController.cancelPrefetch() 210 | } 211 | 212 | contactViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContactViewController") as? ContactViewController 213 | contactViewController!.contactRecord = record 214 | contactViewController!.prefetch() 215 | } 216 | 217 | override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 218 | let record = recordForIndexPath(indexPath) 219 | self.navBar.disableAllTouch() 220 | 221 | tableView.deselectRowAtIndexPath(indexPath, animated: true) 222 | showContactViewControllerForRecord(record) 223 | } 224 | 225 | override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { 226 | if editingStyle == .Delete { 227 | if isSearch { 228 | let record = displayContentArray[indexPath.row] 229 | let index = record.lastName.substringToIndex(record.lastName.startIndex.advancedBy(1)).uppercaseString 230 | var displayArray = contactSectionsDictionary[index]! 231 | displayArray.removeObject(record) 232 | if displayArray.count == 0 { 233 | // remove tile header if there are no more tiles in that group 234 | alphabetArray.removeObject(index) 235 | } 236 | contactSectionsDictionary[index] = displayArray 237 | 238 | // need to delete everything with references to contact before 239 | // removing contact its self 240 | removeContactWithContactId(record.id) 241 | 242 | displayContentArray.removeAtIndex(indexPath.row) 243 | tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) 244 | } else { 245 | let sectionLetter = alphabetArray[indexPath.section] 246 | var sectionContacts = contactSectionsDictionary[sectionLetter]! 247 | let record = sectionContacts[indexPath.row] 248 | 249 | sectionContacts.removeAtIndex(indexPath.row) 250 | contactSectionsDictionary[sectionLetter] = sectionContacts 251 | 252 | tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) 253 | if sectionContacts.count == 0 { 254 | alphabetArray.removeAtIndex(indexPath.section) 255 | } 256 | 257 | removeContactWithContactId(record.id) 258 | } 259 | } 260 | } 261 | 262 | // MARK: - Private methods 263 | 264 | private func recordForIndexPath(indexPath: NSIndexPath) -> ContactRecord { 265 | var record: ContactRecord! 266 | if isSearch { 267 | record = displayContentArray[indexPath.row] 268 | } else { 269 | let sectionContacts = contactSectionsDictionary[alphabetArray[indexPath.section]]! 270 | record = sectionContacts[indexPath.row] 271 | } 272 | return record 273 | } 274 | 275 | private func getContactsListFromServerWithRelation() { 276 | 277 | RESTEngine.sharedEngine.getContactsListFromServerWithRelationWithGroupId(groupRecord.id, success: { response in 278 | 279 | self.alphabetArray = [] 280 | self.contactSectionsDictionary = [:] 281 | self.displayContentArray.removeAll() 282 | 283 | // handle repeat contact-group relationships 284 | var tmpContactIdList: [NSNumber] = [] 285 | 286 | /* 287 | * Structure of reply is: 288 | * { 289 | * record:[ 290 | * { 291 | * , 292 | * contact_by_contact_id:{ 293 | * 294 | * } 295 | * }, 296 | * ... 297 | * ] 298 | * } 299 | */ 300 | let records = response!["resource"] as! JSONArray 301 | for relationRecord in records { 302 | let recordInfo = relationRecord["contact_by_contact_id"] as! JSON 303 | let contactId = recordInfo["id"] as! NSNumber 304 | if tmpContactIdList.contains(contactId) { 305 | // a different record already related the group-contact pair 306 | continue 307 | } 308 | tmpContactIdList.append(contactId) 309 | 310 | let newRecord = ContactRecord(json: recordInfo) 311 | if !newRecord.lastName.isEmpty { 312 | var found = false 313 | for key in self.contactSectionsDictionary.keys { 314 | // want to group by last name regardless of case 315 | if key.caseInsensitiveCompare(newRecord.lastName.substringToIndex(newRecord.lastName.startIndex.advancedBy(1))) == .OrderedSame { 316 | 317 | // contact fits in one of the buckets already in the dictionary 318 | var section = self.contactSectionsDictionary[key]! 319 | section.append(newRecord) 320 | self.contactSectionsDictionary[key] = section 321 | found = true 322 | break 323 | } 324 | } 325 | 326 | if !found { 327 | // contact doesn't fit in any of the other buckets, make a new one 328 | let key = newRecord.lastName.substringToIndex(newRecord.lastName.startIndex.advancedBy(1)) 329 | self.contactSectionsDictionary[key] = [newRecord] 330 | } 331 | } 332 | } 333 | 334 | var tmp: [String: [ContactRecord]] = [:] 335 | // sort the sections alphabetically by last name, first name 336 | for key in self.contactSectionsDictionary.keys { 337 | let unsorted = self.contactSectionsDictionary[key]! 338 | let sorted = unsorted.sort({ (one, two) -> Bool in 339 | if one.lastName.caseInsensitiveCompare(two.lastName) == .OrderedSame { 340 | return one.firstName.compare(two.firstName) == NSComparisonResult.OrderedAscending 341 | } 342 | return one.lastName.compare(two.lastName) == NSComparisonResult.OrderedAscending 343 | }) 344 | tmp[key] = sorted 345 | } 346 | self.contactSectionsDictionary = tmp 347 | self.alphabetArray = Array(self.contactSectionsDictionary.keys).sort() 348 | 349 | dispatch_async(dispatch_get_main_queue()) { 350 | if !self.viewReady { 351 | self.viewReady = true 352 | self.viewLock.signal() 353 | self.viewLock.unlock() 354 | } else { 355 | self.tableView.reloadData() 356 | } 357 | } 358 | 359 | }, failure: { error in 360 | if error.code == 400 { 361 | let decode = error.userInfo["error"]?.firstItem as? JSON 362 | let message = decode?["message"] as? String 363 | if message != nil && message!.containsString("Invalid relationship") { 364 | NSLog("Error: table names in relational calls are case sensitive: \(message)") 365 | dispatch_async(dispatch_get_main_queue()) { 366 | Alert.showAlertWithMessage(message!, fromViewController: self) 367 | self.navigationController?.popToRootViewControllerAnimated(true) 368 | } 369 | return 370 | } 371 | } 372 | NSLog("Error getting contacts with relation: \(error)") 373 | dispatch_async(dispatch_get_main_queue()) { 374 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 375 | self.navigationController?.popToRootViewControllerAnimated(true) 376 | } 377 | }) 378 | } 379 | 380 | private func removeContactWithContactId(contactId: NSNumber) { 381 | // finally remove the contact from the database 382 | 383 | RESTEngine.sharedEngine.removeContactWithContactId(contactId, success: { _ in 384 | dispatch_async(dispatch_get_main_queue()) { 385 | self.tableView.reloadData() 386 | } 387 | }, failure: { error in 388 | NSLog("Error deleting contact: \(error)") 389 | dispatch_async(dispatch_get_main_queue()) { 390 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 391 | } 392 | }) 393 | } 394 | 395 | private func showContactViewControllerForRecord(record: ContactRecord) { 396 | goingToShowContactViewController = true 397 | // give the calls on the other end just a little bit of time 398 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 399 | self.contactViewController!.waitToReady() 400 | dispatch_async(dispatch_get_main_queue()) { 401 | self.navigationController?.pushViewController(self.contactViewController!, animated: true) 402 | } 403 | } 404 | } 405 | 406 | private func showContactEditViewController() { 407 | let contactEditViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContactEditViewController") as! ContactEditViewController 408 | // tell the contact list what group it is looking at 409 | contactEditViewController.contactGroupId = groupRecord.id 410 | 411 | self.navigationController?.pushViewController(contactEditViewController, animated: true) 412 | } 413 | 414 | private func showGroupEditViewController() { 415 | let groupAddViewController = self.storyboard?.instantiateViewControllerWithIdentifier("GroupAddViewController") as! GroupAddViewController 416 | groupAddViewController.groupRecord = groupRecord 417 | groupAddViewController.prefetch() 418 | 419 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 420 | 421 | groupAddViewController.waitToReady() 422 | dispatch_async(dispatch_get_main_queue()) { 423 | self.navigationController?.pushViewController(groupAddViewController, animated: true) 424 | } 425 | } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/ContactViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/5/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ContactViewController: UIViewController { 12 | @IBOutlet weak var contactDetailScrollView: UIScrollView! 13 | 14 | // the contact being looked at 15 | var contactRecord: ContactRecord! 16 | 17 | // holds contact info records 18 | private var contactDetails: [ContactDetailRecord]! 19 | 20 | private var contactGroups: [String]! 21 | 22 | private var queue: dispatch_queue_t! 23 | 24 | private var groupLock: NSCondition! 25 | private var groupReady = false 26 | 27 | private var viewLock: NSCondition! 28 | private var viewReady = false 29 | 30 | private var waitLock: NSCondition! 31 | private var waitReady = false 32 | 33 | private var canceled = false 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | contactDetailScrollView.frame = CGRectMake(0, 0, view.frame.size.width, view.frame.size.height) 39 | contactDetailScrollView.backgroundColor = UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0) 40 | } 41 | 42 | override func viewWillAppear(animated: Bool) { 43 | super.viewWillAppear(animated) 44 | if !viewReady { 45 | // only unlock the view if it is locked 46 | viewReady = true 47 | viewLock.signal() 48 | viewLock.unlock() 49 | } 50 | 51 | let navBar = self.navBar 52 | navBar.showEdit() 53 | navBar.editButton.addTarget(self, action: #selector(onEditButtonClick), forControlEvents: .TouchDown) 54 | navBar.enableAllTouch() 55 | } 56 | 57 | override func viewWillDisappear(animated: Bool) { 58 | super.viewWillDisappear(animated) 59 | 60 | self.navBar.editButton.removeTarget(self, action: #selector(onEditButtonClick), forControlEvents: .TouchDown) 61 | } 62 | 63 | func onEditButtonClick() { 64 | let contactEditViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContactEditViewController") as! ContactEditViewController 65 | // tell the contact list what group it is looking at 66 | contactEditViewController.contactRecord = contactRecord 67 | contactEditViewController.contactViewController = self 68 | contactEditViewController.contactDetails = contactDetails 69 | 70 | self.navigationController?.pushViewController(contactEditViewController, animated: true) 71 | } 72 | 73 | func prefetch() { 74 | contactDetails = [] 75 | contactGroups = [] 76 | 77 | groupLock = NSCondition() 78 | waitLock = NSCondition() 79 | viewLock = NSCondition() 80 | 81 | groupReady = false 82 | waitReady = false 83 | viewReady = false 84 | canceled = false 85 | 86 | dispatch_async(dispatch_get_main_queue()) { 87 | self.waitLock.lock() 88 | self.groupLock.lock() 89 | self.viewLock.lock() 90 | } 91 | 92 | queue = dispatch_queue_create("contactViewQueue", nil) 93 | dispatch_async(queue) {[weak self] in 94 | if let strongSelf = self { 95 | strongSelf.getContactInfoFromServerForRecord(strongSelf.contactRecord) 96 | } 97 | } 98 | dispatch_async(queue) {[weak self] in 99 | if let strongSelf = self { 100 | strongSelf.getContactsListFromServerWithRelation() 101 | } 102 | } 103 | dispatch_async(queue) {[weak self] in 104 | if let strongSelf = self { 105 | strongSelf.buildContactView() 106 | } 107 | } 108 | } 109 | 110 | func cancelPrefetch() { 111 | canceled = true 112 | dispatch_async(dispatch_get_main_queue()) { 113 | self.viewReady = true 114 | self.viewLock.signal() 115 | self.viewLock.unlock() 116 | } 117 | } 118 | 119 | func waitToReady() { 120 | waitLock.lock() 121 | while !waitReady { 122 | waitLock.wait() 123 | } 124 | waitLock.signal() 125 | waitLock.unlock() 126 | } 127 | 128 | // MARK: - Private methods 129 | 130 | // build the address boxes 131 | private func buildAddressViewForRecord(record: ContactDetailRecord, y: CGFloat, buffer: CGFloat) -> UIView { 132 | 133 | let subView = UIView(frame: CGRectMake(0, y + buffer, view.frame.size.width, 20)) 134 | subView.translatesAutoresizingMaskIntoConstraints = false 135 | 136 | let typeLabel = UILabel(frame: CGRectMake(25, 10, subView.frame.size.width - 25, 30)) 137 | typeLabel.text = record.type 138 | typeLabel.font = UIFont(name: "HelveticaNeue-Light", size: 23.0) 139 | typeLabel.textColor = UIColor(red: 253/255.0, green: 253/255.0, blue: 250/255.0, alpha: 1.0) 140 | subView.addSubview(typeLabel) 141 | 142 | var next_y: CGFloat = 40 // track where the lowest item is 143 | if !record.email.isEmpty { 144 | let label = UILabel(frame: CGRectMake(50, 40, subView.frame.size.width - 50, 30)) 145 | label.text = record.email 146 | label.font = UIFont(name: "HelveticaNeue-Light", size: 20.0) 147 | label.textColor = UIColor(red: 249/255.0, green: 249/255.0, blue: 249/255.0, alpha: 1.0) 148 | subView.addSubview(label) 149 | 150 | let imageView = UIImageView(frame: CGRectMake(25, 45, 20, 20)) 151 | imageView.image = UIImage(named: "mail") 152 | imageView.contentMode = .ScaleAspectFit 153 | subView.addSubview(imageView) 154 | 155 | next_y += 60 156 | } 157 | 158 | if !record.phone.isEmpty { 159 | let label = UILabel(frame: CGRectMake(50, next_y, subView.frame.size.width - 50, 20)) 160 | label.text = record.phone 161 | label.font = UIFont(name: "HelveticaNeue-Light", size: 20.0) 162 | label.textColor = UIColor(red: 253/255.0, green: 253/255.0, blue: 250/255.0, alpha: 1.0) 163 | subView.addSubview(label) 164 | 165 | let imageView = UIImageView(frame: CGRectMake(25, next_y, 20, 20)) 166 | imageView.image = UIImage(named: "phone1") 167 | imageView.contentMode = .ScaleAspectFit 168 | subView.addSubview(imageView) 169 | 170 | next_y += 60 171 | } 172 | 173 | if !record.address.isEmpty && !record.city.isEmpty && !record.state.isEmpty && !record.zipCode.isEmpty { 174 | let label = UILabel(frame: CGRectMake(50, next_y, subView.frame.size.width - 50, 20)) 175 | label.font = UIFont(name: "HelveticaNeue-Light", size: 19.0) 176 | label.numberOfLines = 0 177 | label.textColor = UIColor(red: 250/255.0, green: 250/255.0, blue: 250/255.0, alpha: 1.0) 178 | 179 | var addressText = "\(record.address)\n\(record.city), \(record.state) \(record.zipCode)" 180 | if !record.country.isEmpty { 181 | addressText += "\n\(record.country)" 182 | } 183 | label.text = addressText 184 | label.sizeToFit() 185 | subView.addSubview(label) 186 | 187 | let imageView = UIImageView(frame: CGRectMake(25, next_y, 20, 20)) 188 | imageView.image = UIImage(named: "home") 189 | imageView.contentMode = .ScaleAspectFit 190 | subView.addSubview(imageView) 191 | 192 | next_y += label.frame.size.height 193 | } 194 | 195 | // resize the subview 196 | var viewFrame = subView.frame 197 | viewFrame.size.height = next_y + 20 198 | viewFrame.origin.x = view.frame.size.width * 0.06 199 | viewFrame.size.width = view.frame.size.width * 0.88 200 | subView.frame = viewFrame 201 | 202 | return subView 203 | } 204 | 205 | private func makeListOfGroupsContactBelongsTo() -> UIView { 206 | let subView = UIView(frame: CGRectMake(0, 0, view.frame.size.width, 20)) 207 | 208 | let typeLabel = UILabel(frame: CGRectMake(0, 10, subView.frame.size.width - 25, 30)) 209 | typeLabel.text = "Groups:" 210 | typeLabel.font = UIFont(name: "HelveticaNeue-Light", size: 23.0) 211 | subView.addSubview(typeLabel) 212 | 213 | var y: CGFloat = 50 214 | for groupName in contactGroups { 215 | let label = UILabel(frame: CGRectMake(25, y, subView.frame.size.width - 75, 30)) 216 | label.text = groupName 217 | label.font = UIFont(name: "HelveticaNeue-Light", size: 20.0) 218 | 219 | y += 30 220 | 221 | subView.addSubview(label) 222 | } 223 | 224 | // resize the subview 225 | var viewFrame = subView.frame 226 | viewFrame.size.height = y + 20 227 | viewFrame.origin.x = view.frame.size.width * 0.06 228 | viewFrame.size.width = view.frame.size.width * 0.88 229 | subView.frame = viewFrame 230 | 231 | return subView 232 | } 233 | 234 | private func buildContactView() { 235 | // clear out the view 236 | dispatch_sync(dispatch_get_main_queue()) { 237 | if let contactDetailScrollView = self.contactDetailScrollView { 238 | for view in contactDetailScrollView.subviews { 239 | view.removeFromSuperview() 240 | } 241 | } 242 | } 243 | 244 | // get the profile image 245 | let profileImageView = UIImageView(frame: CGRectMake(0, 0, view.frame.size.width * 0.6, view.frame.size.width * 0.5)) 246 | getProfileImageFromServerToImageView(profileImageView) 247 | 248 | viewLock.lock() 249 | while !viewReady { 250 | viewLock.wait() 251 | } 252 | viewLock.unlock() 253 | 254 | if canceled { 255 | return 256 | } 257 | 258 | // track the y of the furthest item down in the view 259 | var y: CGFloat = 0 260 | dispatch_sync(dispatch_get_main_queue()) { 261 | profileImageView.center = CGPointMake(self.view.frame.size.width * 0.5, profileImageView.frame.size.height * 0.5) 262 | y = profileImageView.frame.size.height + 5.0 263 | 264 | // add the name label 265 | let nameLabel = UILabel(frame: CGRectMake(0, y, self.view.frame.size.width, 35)) 266 | nameLabel.text = self.contactRecord.fullName 267 | nameLabel.font = UIFont(name: "HelveticaNeue-Light", size: 25.0) 268 | nameLabel.textAlignment = .Center 269 | self.contactDetailScrollView.addSubview(nameLabel) 270 | y += 40 271 | 272 | if !self.contactRecord.twitter.isEmpty { 273 | let label = UILabel(frame: CGRectMake(40, y, self.view.frame.size.width - 40, 20)) 274 | label.font = UIFont(name: "Helvetica Neue", size: 17.0) 275 | label.text = self.contactRecord.twitter 276 | self.contactDetailScrollView.addSubview(label) 277 | 278 | let imageView = UIImageView(frame: CGRectMake(10, y, 20, 20)) 279 | imageView.image = UIImage(named: "twitter2") 280 | imageView.contentMode = .ScaleAspectFit 281 | self.contactDetailScrollView.addSubview(imageView) 282 | 283 | y += 30 284 | } 285 | 286 | if !self.contactRecord.skype.isEmpty { 287 | let label = UILabel(frame: CGRectMake(40, y, self.view.frame.size.width - 40, 20)) 288 | label.font = UIFont(name: "Helvetica Neue", size: 17.0) 289 | label.text = self.contactRecord.skype 290 | self.contactDetailScrollView.addSubview(label) 291 | 292 | let imageView = UIImageView(frame: CGRectMake(10, y, 20, 20)) 293 | imageView.image = UIImage(named: "skype") 294 | imageView.contentMode = .ScaleAspectFit 295 | self.contactDetailScrollView.addSubview(imageView) 296 | 297 | y += 30 298 | } 299 | 300 | if !self.contactRecord.notes.isEmpty { 301 | let label = UILabel(frame: CGRectMake(10, y, 80, 25)) 302 | label.font = UIFont(name: "HelveticaNeue-Light", size: 19.0) 303 | label.text = "Notes" 304 | self.contactDetailScrollView.addSubview(label) 305 | y += 20 306 | 307 | let notesLabel = UILabel(frame: CGRectMake(self.view.frame.size.width * 0.05, y, self.view.frame.size.width * 0.9, 80)) 308 | notesLabel.autoresizesSubviews = false 309 | notesLabel.font = UIFont(name: "Helvetica Neue", size: 16.0) 310 | notesLabel.numberOfLines = 0 311 | notesLabel.text = self.contactRecord.notes 312 | notesLabel.sizeToFit() 313 | self.contactDetailScrollView.addSubview(notesLabel) 314 | 315 | y += notesLabel.frame.size.height + 10 316 | } 317 | 318 | // add all the addresses 319 | for record in self.contactDetails { 320 | let toAdd = self.buildAddressViewForRecord(record, y: y, buffer: 25) 321 | toAdd.backgroundColor = UIColor(red: 112/255.0, green: 147/255.0, blue: 181/255.0, alpha: 1.0) 322 | y += toAdd.frame.size.height + 25 323 | self.contactDetailScrollView.addSubview(toAdd) 324 | } 325 | } 326 | 327 | // wait until the group is ready to build group list subviews 328 | groupLock.lock() 329 | while !groupReady { 330 | groupLock.wait() 331 | } 332 | groupLock.unlock() 333 | 334 | dispatch_sync(dispatch_get_main_queue()) { 335 | let toAdd = self.makeListOfGroupsContactBelongsTo() 336 | var frame = toAdd.frame 337 | frame.origin.y = y + 20 338 | toAdd.frame = frame 339 | 340 | self.contactDetailScrollView.addSubview(toAdd) 341 | 342 | // resize the scroll view content 343 | var contectRect = CGRectZero 344 | for view in self.contactDetailScrollView.subviews { 345 | contectRect = CGRectUnion(contectRect, view.frame) 346 | } 347 | self.contactDetailScrollView.contentSize = CGSizeMake(self.contactDetailScrollView.frame.size.width, contectRect.size.height) 348 | self.contactDetailScrollView.reloadInputViews() 349 | } 350 | } 351 | 352 | private func getContactInfoFromServerForRecord(record: ContactRecord) { 353 | RESTEngine.sharedEngine.getContactInfoFromServerWithContactId(record.id, success: { response in 354 | // put the contact ids into an array 355 | var array: [ContactDetailRecord] = [] 356 | 357 | // double check we don't fetch any repeats 358 | var existingIds: [NSNumber: Bool] = [:] 359 | 360 | let records = response!["resource"] as! JSONArray 361 | for recordInfo in records { 362 | let recordId = recordInfo["id"] as! NSNumber 363 | if existingIds[recordId] != nil { 364 | continue 365 | } 366 | existingIds[recordId] = true 367 | 368 | let newRecord = ContactDetailRecord(json: recordInfo) 369 | array.append(newRecord) 370 | } 371 | 372 | self.contactDetails = array 373 | dispatch_async(dispatch_get_main_queue()) { 374 | self.waitReady = true 375 | self.waitLock.signal() 376 | self.waitLock.unlock() 377 | } 378 | 379 | }, failure: { error in 380 | NSLog("Error getting contact info: \(error)") 381 | dispatch_async(dispatch_get_main_queue()) { 382 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 383 | self.navigationController?.popToRootViewControllerAnimated(true) 384 | } 385 | }) 386 | } 387 | 388 | private func getProfileImageFromServerToImageView(imageView: UIImageView) { 389 | if contactRecord.imageURL == nil || contactRecord.imageURL.isEmpty { 390 | dispatch_async(dispatch_get_main_queue()) { 391 | imageView.image = UIImage(named: "default_portrait") 392 | imageView.contentMode = .ScaleAspectFit 393 | self.contactDetailScrollView.addSubview(imageView) 394 | } 395 | return 396 | } 397 | 398 | RESTEngine.sharedEngine.getProfileImageFromServerWithContactId(contactRecord.id, fileName: contactRecord.imageURL, success: { response in 399 | dispatch_async(dispatch_get_main_queue()) { 400 | var image: UIImage! 401 | guard let content = response?["content"] as? String, 402 | let fileData = NSData(base64EncodedString: content, options: [NSDataBase64DecodingOptions.IgnoreUnknownCharacters]) 403 | else { 404 | NSLog("\nWARN: Could not load image off of server, loading default\n"); 405 | image = UIImage(named: "default_portrait") 406 | imageView.image = image 407 | imageView.contentMode = .ScaleAspectFit 408 | self.contactDetailScrollView.addSubview(imageView) 409 | return 410 | } 411 | 412 | image = UIImage(data: fileData) 413 | imageView.image = image 414 | imageView.contentMode = .ScaleAspectFit 415 | self.contactDetailScrollView.addSubview(imageView) 416 | } 417 | 418 | }, failure: { error in 419 | NSLog("Error getting profile image data from server: \(error)") 420 | dispatch_async(dispatch_get_main_queue()) { 421 | NSLog("\nWARN: Could not load image off of server, loading default\n"); 422 | let image = UIImage(named: "default_portrait") 423 | imageView.image = image 424 | imageView.contentMode = .ScaleAspectFit 425 | self.contactDetailScrollView.addSubview(imageView) 426 | } 427 | }) 428 | } 429 | 430 | private func getContactsListFromServerWithRelation() { 431 | 432 | RESTEngine.sharedEngine.getContactGroupsWithContactId(contactRecord.id, success: { response in 433 | self.contactGroups.removeAll() 434 | 435 | // handle repeat contact-group relationships 436 | var tmpGroupIdMap: [NSNumber: Bool] = [:] 437 | 438 | /* 439 | * Structure of reply is: 440 | * { 441 | * record:[ 442 | * { 443 | * , 444 | * contact_group_by_contactGroupId:{ 445 | * 446 | * } 447 | * }, 448 | * ... 449 | * ] 450 | * } 451 | */ 452 | 453 | let records = response!["resource"] as! JSONArray 454 | for relationalRecord in records { 455 | let recordInfo = relationalRecord["contact_group_by_contact_group_id"] as! JSON 456 | let contactId = recordInfo["id"] as! NSNumber 457 | if tmpGroupIdMap[contactId] != nil { 458 | // a different record already related the group-contact pair 459 | continue 460 | } 461 | tmpGroupIdMap[contactId] = true 462 | let groupName = recordInfo["name"] as! String 463 | self.contactGroups.append(groupName) 464 | } 465 | dispatch_async(dispatch_get_main_queue()) { 466 | self.groupReady = true 467 | self.groupLock.signal() 468 | self.groupLock.unlock() 469 | } 470 | 471 | }, failure: { error in 472 | NSLog("Error getting groups with relation: \(error)") 473 | dispatch_async(dispatch_get_main_queue()) { 474 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 475 | self.navigationController?.popToRootViewControllerAnimated(true) 476 | } 477 | }) 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/GroupAddViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupAddViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/5/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GroupAddViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UITextFieldDelegate { 12 | @IBOutlet weak var groupAddTableView: UITableView! 13 | @IBOutlet weak var groupNameTextField: UITextField! 14 | 15 | // set groupRecord and contacts alreadyInGroup when editing an existing group 16 | // contacts already in the existing group 17 | var contactsAlreadyInGroupContentsArray: [NSNumber]! 18 | // record of the group being edited 19 | var groupRecord: GroupRecord? 20 | 21 | private var searchBar: UISearchBar! 22 | 23 | // if there is a search going on 24 | private var isSearch = false 25 | 26 | // holds contents of a search 27 | private var displayContentArray: [ContactRecord]! 28 | 29 | // array of contacts selected to be in the group 30 | private var selectedRows: [NSNumber]! 31 | 32 | // contacts broken into groups by first letter of last name 33 | private var contactSectionsDictionary: [String: [ContactRecord]]! 34 | 35 | // header letters 36 | private var alphabetArray: [String]! 37 | 38 | private var waitLock: NSCondition! 39 | private var waitReady = false 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | groupNameTextField.delegate = self 45 | groupNameTextField.setValue(UIColor(red: 180/255.0, green: 180/255.0, blue: 180/255.0, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor") 46 | 47 | // set up the search bar programmatically 48 | searchBar = UISearchBar(frame: CGRectMake(0, 118, view.frame.size.width, 44)) 49 | searchBar.delegate = self 50 | self.view.addSubview(searchBar) 51 | searchBar.setNeedsDisplay() 52 | self.view.reloadInputViews() 53 | 54 | if let groupRecord = groupRecord { 55 | // if we are editing a group, get any existing group members 56 | groupNameTextField.text = groupRecord.name 57 | } 58 | // remove header from table view 59 | self.automaticallyAdjustsScrollViewInsets = false 60 | } 61 | 62 | override func viewWillAppear(animated: Bool) { 63 | super.viewWillAppear(animated) 64 | 65 | let navBar = self.navBar 66 | navBar.showDone() 67 | navBar.doneButton.addTarget(self, action: #selector(onDoneButtonClick), forControlEvents: .TouchDown) 68 | navBar.enableAllTouch() 69 | } 70 | 71 | override func viewWillDisappear(animated: Bool) { 72 | super.viewWillDisappear(animated) 73 | 74 | self.navBar.doneButton.removeTarget(self, action: #selector(onDoneButtonClick), forControlEvents: .TouchDown) 75 | navBar.disableAllTouch() 76 | } 77 | 78 | func onDoneButtonClick() { 79 | if groupNameTextField.text?.characters.count == 0 { 80 | let alert = UIAlertController(title: nil, message: "Please enter a group name", preferredStyle: .Alert) 81 | alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) 82 | presentViewController(alert, animated: true, completion: nil) 83 | return 84 | } 85 | 86 | if groupRecord != nil { 87 | // if we are editing a group, head to update 88 | buildUpdateRequest() 89 | } else { 90 | addNewGroupToServer() 91 | } 92 | } 93 | 94 | func prefetch() { 95 | displayContentArray = [] 96 | contactsAlreadyInGroupContentsArray = [] 97 | selectedRows = [] 98 | 99 | // for sectioned alphabetized list 100 | alphabetArray = [] 101 | contactSectionsDictionary = [:] 102 | 103 | waitLock = NSCondition() 104 | waitLock.lock() 105 | waitReady = false 106 | 107 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 108 | self.getContactListFromServer() 109 | if self.groupRecord != nil { 110 | // if we are editing a group, get any existing group members 111 | self.getContactGroupRelationListFromServer() 112 | } 113 | } 114 | self.navBar.disableAllTouch() 115 | } 116 | 117 | func waitToReady() { 118 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 119 | self.waitLock.lock() 120 | while !self.waitReady { 121 | self.waitLock.wait() 122 | } 123 | self.waitLock.unlock() 124 | } 125 | } 126 | 127 | //MARK: - Text field delegate 128 | 129 | func textFieldShouldReturn(textField: UITextField) -> Bool { 130 | textField.resignFirstResponder() 131 | return true 132 | } 133 | 134 | //MARK: - Search bar delegate 135 | 136 | func searchBar(searchBar: UISearchBar, textDidChange searchText: String) { 137 | displayContentArray.removeAll() 138 | 139 | if searchText.isEmpty { 140 | // done with searching, show all the data 141 | isSearch = false 142 | groupAddTableView.reloadData() 143 | return 144 | } 145 | isSearch = true 146 | let firstLetter = searchText.substringToIndex(searchText.startIndex.advancedBy(1)).uppercaseString 147 | let arrayAtLetter = contactSectionsDictionary[firstLetter] 148 | if let arrayAtLetter = arrayAtLetter { 149 | for record in arrayAtLetter { 150 | if record.lastName.characters.count < searchText.characters.count { 151 | continue 152 | } 153 | let lastNameSubstring = record.lastName.substringToIndex(record.lastName.startIndex.advancedBy(searchText.characters.count)) 154 | if lastNameSubstring.caseInsensitiveCompare(searchText) == .OrderedSame { 155 | displayContentArray.append(record) 156 | } 157 | } 158 | groupAddTableView.reloadData() 159 | } 160 | } 161 | 162 | func searchBarSearchButtonClicked(searchBar: UISearchBar) { 163 | searchBar.resignFirstResponder() 164 | } 165 | 166 | func searchBarCancelButtonClicked(searchBar: UISearchBar) { 167 | searchBar.resignFirstResponder() 168 | } 169 | 170 | //MARK: - Table view data source 171 | 172 | func numberOfSectionsInTableView(tableView: UITableView) -> Int { 173 | if isSearch { 174 | return 1 175 | } 176 | return alphabetArray.count 177 | } 178 | 179 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 180 | if isSearch { 181 | return displayContentArray.count 182 | } 183 | let sectionContacts = contactSectionsDictionary[alphabetArray[section]]! 184 | return sectionContacts.count 185 | } 186 | 187 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 188 | let cell = tableView.dequeueReusableCellWithIdentifier("addGroupContactTableViewCell", forIndexPath: indexPath) 189 | 190 | var record: ContactRecord! 191 | if isSearch { 192 | record = displayContentArray[indexPath.row] 193 | } else { 194 | let sectionContacts = contactSectionsDictionary[alphabetArray[indexPath.section]]! 195 | record = sectionContacts[indexPath.row] 196 | } 197 | 198 | cell.textLabel?.text = record.fullName 199 | 200 | // if the contact is selected to be in the group, mark it 201 | if selectedRows.contains(record.id) { 202 | cell.accessoryType = .Checkmark 203 | } else { 204 | cell.accessoryType = .None 205 | } 206 | 207 | return cell 208 | } 209 | 210 | //MARK: - Table view delegate 211 | 212 | func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 213 | if isSearch { 214 | let searchText = searchBar.text 215 | if searchText?.characters.count > 0 { 216 | return searchText!.substringToIndex(searchText!.startIndex.advancedBy(1)).uppercaseString 217 | } 218 | } 219 | return alphabetArray[section] 220 | } 221 | 222 | func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 223 | view.tintColor = UIColor(red: 210/255.0, green: 225/255.0, blue: 239/255.0, alpha: 1.0) 224 | } 225 | 226 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 227 | let cell = tableView.cellForRowAtIndexPath(indexPath)! 228 | var contact: ContactRecord! 229 | 230 | if isSearch { 231 | contact = displayContentArray[indexPath.row] 232 | } else { 233 | let sectionContacts = contactSectionsDictionary[alphabetArray[indexPath.section]]! 234 | contact = sectionContacts[indexPath.row] 235 | } 236 | 237 | if cell.accessoryType == .None { 238 | cell.accessoryType = .Checkmark 239 | selectedRows.append(contact.id) 240 | } else { 241 | cell.accessoryType = .None 242 | selectedRows.removeObject(contact.id) 243 | } 244 | 245 | tableView.deselectRowAtIndexPath(indexPath, animated: true) 246 | } 247 | 248 | // MARK: - Private methods 249 | 250 | private func buildUpdateRequest() { 251 | // if a contact is selected and was already in the group, do nothing 252 | // if a contact is selected and was not in the group, add it to the group 253 | // if a contact is not selected and was in the group, remove it from the group 254 | 255 | // removing an object mid loop messes with for-each loop 256 | var toRemove: [NSNumber] = [] 257 | for contactId in selectedRows { 258 | for i in 0.. add contacts to group 273 | updateGroupWithServer() 274 | } 275 | 276 | private func getContactListFromServer() { 277 | 278 | RESTEngine.sharedEngine.getContactListFromServerWithSuccess({ response in 279 | // put the contact ids into an array 280 | let records = response!["resource"] as! JSONArray 281 | for recordInfo in records { 282 | let newRecord = ContactRecord(json: recordInfo) 283 | 284 | if !newRecord.lastName.isEmpty { 285 | var found = false 286 | for key in self.contactSectionsDictionary.keys { 287 | // want to group by last name regardless of case 288 | if key.caseInsensitiveCompare(newRecord.lastName.substringToIndex(newRecord.lastName.startIndex.advancedBy(1))) == .OrderedSame { 289 | var section = self.contactSectionsDictionary[key]! 290 | section.append(newRecord) 291 | self.contactSectionsDictionary[key] = section 292 | found = true 293 | break 294 | } 295 | } 296 | 297 | if !found { 298 | // contact doesn't fit in any of the other buckets, make a new one 299 | let key = newRecord.lastName.substringToIndex(newRecord.lastName.startIndex.advancedBy(1)) 300 | self.contactSectionsDictionary[key] = [newRecord] 301 | } 302 | } 303 | } 304 | 305 | var tmp: [String: [ContactRecord]] = [:] 306 | // sort the sections alphabetically by last name, first name 307 | for key in self.contactSectionsDictionary.keys { 308 | let unsorted = self.contactSectionsDictionary[key]! 309 | let sorted = unsorted.sort({ (one, two) -> Bool in 310 | if one.lastName.caseInsensitiveCompare(two.lastName) == .OrderedSame { 311 | return one.firstName.compare(two.firstName) == NSComparisonResult.OrderedAscending 312 | } 313 | return one.lastName.compare(two.lastName) == NSComparisonResult.OrderedAscending 314 | }) 315 | tmp[key] = sorted 316 | } 317 | self.contactSectionsDictionary = tmp 318 | self.alphabetArray = Array(self.contactSectionsDictionary.keys).sort() 319 | 320 | dispatch_async(dispatch_get_main_queue()) { 321 | self.waitReady = true 322 | self.waitLock.signal() 323 | self.waitLock.unlock() 324 | 325 | self.groupAddTableView.reloadData() 326 | } 327 | 328 | }, failure: { error in 329 | NSLog("Error getting all the contacts data: \(error)") 330 | dispatch_async(dispatch_get_main_queue()) { 331 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 332 | self.navigationController?.popToRootViewControllerAnimated(true) 333 | } 334 | }) 335 | } 336 | 337 | private func addNewGroupToServer() { 338 | RESTEngine.sharedEngine.addGroupToServerWithName(groupNameTextField.text!, contactIds: selectedRows, success: {_ in 339 | dispatch_async(dispatch_get_main_queue()) { 340 | // go to previous screen 341 | self.navigationController?.popViewControllerAnimated(true) 342 | } 343 | }, failure: { error in 344 | NSLog("Error adding group to server: \(error)") 345 | dispatch_async(dispatch_get_main_queue()) { 346 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 347 | self.navigationController?.popToRootViewControllerAnimated(true) 348 | } 349 | }) 350 | } 351 | 352 | private func updateGroupWithServer() { 353 | RESTEngine.sharedEngine.updateGroupWithId(groupRecord!.id, name: groupNameTextField.text!, oldName: groupRecord!.name, removedContactIds: contactsAlreadyInGroupContentsArray, addedContactIds: selectedRows, success: { _ in 354 | 355 | self.groupRecord!.name = self.groupNameTextField.text! 356 | dispatch_async(dispatch_get_main_queue()) { 357 | self.navigationController?.popViewControllerAnimated(true) 358 | } 359 | }, failure: { error in 360 | NSLog("Error updating contact info with server: \(error)") 361 | dispatch_async(dispatch_get_main_queue()) { 362 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 363 | self.navigationController?.popToRootViewControllerAnimated(true) 364 | } 365 | }) 366 | } 367 | 368 | private func getContactGroupRelationListFromServer() { 369 | RESTEngine.sharedEngine.getContactGroupRelationListFromServerWithGroupId(groupRecord!.id, success: { response in 370 | 371 | self.contactsAlreadyInGroupContentsArray.removeAll() 372 | let records = response!["resource"] as! JSONArray 373 | for recordInfo in records { 374 | let contactId = recordInfo["contact_id"] as! NSNumber 375 | self.contactsAlreadyInGroupContentsArray.append(contactId) 376 | self.selectedRows.append(contactId) 377 | } 378 | dispatch_async(dispatch_get_main_queue()) { 379 | self.groupAddTableView.reloadData() 380 | } 381 | 382 | }, failure: { error in 383 | NSLog("Error getting contact group relations list: \(error)") 384 | dispatch_async(dispatch_get_main_queue()) { 385 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 386 | self.navigationController?.popToRootViewControllerAnimated(true) 387 | } 388 | }) 389 | } 390 | } 391 | 392 | // helper extension to remove objects from array 393 | extension Array where Element: Equatable { 394 | mutating func removeObject(object: Element) { 395 | if let index = self.indexOf(object) { 396 | self.removeAtIndex(index) 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/MasterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MasterViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MasterViewController: UIViewController { 12 | 13 | @IBOutlet weak var emailTextField: UITextField! 14 | @IBOutlet weak var passwordTextField: UITextField! 15 | @IBOutlet weak var versionLabel: UILabel! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | versionLabel.text = "Version \(kAppVersion)" 21 | 22 | // check if login credentials are already stored 23 | let userEmail = NSUserDefaults.standardUserDefaults().valueForKey(kUserEmail) as? String 24 | let userPassword = NSUserDefaults.standardUserDefaults().valueForKey(kPassword) as? String 25 | 26 | emailTextField.setValue(UIColor(red: 180/255.0, green: 180/255.0, blue: 180/255.0, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor") 27 | passwordTextField.setValue(UIColor(red: 180/255.0, green: 180/255.0, blue: 180/255.0, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor") 28 | 29 | if userEmail?.characters.count > 0 && userPassword?.characters.count > 0 { 30 | emailTextField.text = userEmail 31 | passwordTextField.text = userPassword 32 | } 33 | 34 | navBar.backButton.addTarget(self, action: #selector(onBackButtonClick), forControlEvents: .TouchDown) 35 | } 36 | 37 | func onBackButtonClick() { 38 | self.navigationController?.popViewControllerAnimated(true) 39 | } 40 | 41 | override func viewWillAppear(animated: Bool) { 42 | super.viewWillAppear(animated) 43 | 44 | let navBar = self.navBar 45 | navBar.showBackButton(false) 46 | navBar.showAddButton(false) 47 | navBar.showEditButton(false) 48 | navBar.showDoneButton(false) 49 | } 50 | override func viewDidAppear(animated: Bool) { 51 | if !RESTEngine.sharedEngine.isConfigured() { 52 | Alert.showAlertWithMessage("RESTEngine is not configured.\n\nPlease see README.md.", fromViewController: self) 53 | } 54 | } 55 | 56 | @IBAction func onRegisterClick(sender: AnyObject) { 57 | showRegisterViewController() 58 | } 59 | 60 | @IBAction func onSignInClick(sender: AnyObject) { 61 | self.view.endEditing(true) 62 | 63 | //log in using the generic API 64 | if emailTextField.text?.characters.count > 0 && passwordTextField.text?.characters.count > 0 { 65 | 66 | RESTEngine.sharedEngine.loginWithEmail(emailTextField.text!, password: passwordTextField.text!, 67 | success: { response in 68 | RESTEngine.sharedEngine.sessionToken = response!["session_token"] as? String 69 | let defaults = NSUserDefaults.standardUserDefaults() 70 | defaults.setValue(self.emailTextField.text!, forKey: kUserEmail) 71 | defaults.setValue(self.passwordTextField.text!, forKey: kPassword) 72 | defaults.synchronize() 73 | dispatch_async(dispatch_get_main_queue()) { 74 | self.showAddressBookViewController() 75 | } 76 | }, failure: { error in 77 | NSLog("Error logging in user: \(error)") 78 | dispatch_async(dispatch_get_main_queue()) { 79 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 80 | } 81 | }) 82 | } else { 83 | Alert.showAlertWithMessage("Enter email and password", fromViewController: self) 84 | } 85 | } 86 | 87 | private func showAddressBookViewController() { 88 | let addressBookViewController = self.storyboard?.instantiateViewControllerWithIdentifier("AddressBookViewController") 89 | self.navigationController?.pushViewController(addressBookViewController!, animated: true) 90 | } 91 | 92 | private func showRegisterViewController() { 93 | let registerViewController = self.storyboard?.instantiateViewControllerWithIdentifier("RegisterViewController") 94 | self.navigationController?.pushViewController(registerViewController!, animated: true) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/ProfileImagePickerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileImagePickerViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/5/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ProfileImagePickerDelegate: class { 12 | func didSelectItem(item: String) 13 | func didSelectItem(item: String, withImage image: UIImage) 14 | } 15 | 16 | class ProfileImagePickerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 17 | @IBOutlet var tableView: UITableView! 18 | @IBOutlet var imageNameTextField: UITextField! 19 | 20 | // set only when editing an existing contact 21 | // the contact we are choosing a profile image for 22 | var record: ContactRecord! 23 | 24 | // holds image picked from the camera roll 25 | var imageToUpload: UIImage? 26 | 27 | // list of available profile images 28 | var imageListContentArray: [String] = [] 29 | 30 | weak var delegate: ProfileImagePickerDelegate? 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | getImageListFromServer() 35 | 36 | imageNameTextField.setValue(UIColor(red: 180/255.0, green: 180/255.0, blue: 180/255.0, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor") 37 | tableView.contentInset = UIEdgeInsetsMake(-70, 0, -20, 0) 38 | } 39 | 40 | override func viewWillAppear(animated: Bool) { 41 | super.viewWillAppear(animated) 42 | 43 | let navBar = self.navBar 44 | navBar.showDone() 45 | self.navBar.doneButton.addTarget(self, action: #selector(onDoneButtonClick), forControlEvents: .TouchDown) 46 | } 47 | 48 | override func viewWillDisappear(animated: Bool) { 49 | super.viewWillDisappear(animated) 50 | 51 | self.navBar.doneButton.removeTarget(self, action: #selector(onDoneButtonClick), forControlEvents: .TouchDown) 52 | } 53 | 54 | @IBAction func onChooseImageClick() { 55 | let imagePicker = UIImagePickerController() 56 | imagePicker.sourceType = .PhotoLibrary 57 | imagePicker.delegate = self 58 | presentViewController(imagePicker, animated: true, completion: nil) 59 | } 60 | 61 | func onDoneButtonClick() { 62 | // actually put image up on the server when the contact gets created 63 | if let imageToUpload = imageToUpload { 64 | // if we chose an image to upload 65 | var imageName: String! 66 | if imageNameTextField.text!.isEmpty { 67 | imageName = "profileImage" 68 | } else { 69 | imageName = imageNameTextField.text! 70 | } 71 | delegate?.didSelectItem(imageName, withImage: imageToUpload) 72 | } else { 73 | self.navigationController?.popViewControllerAnimated(true) 74 | } 75 | } 76 | 77 | //MARK: - Image picker delegate 78 | 79 | func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) { 80 | let image = info[UIImagePickerControllerOriginalImage] as! UIImage 81 | imageToUpload = image 82 | 83 | dismissViewControllerAnimated(true, completion: nil) 84 | } 85 | 86 | //MARK: - Tableview data source 87 | 88 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 89 | return imageListContentArray.count 90 | } 91 | 92 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 93 | let cell = tableView.dequeueReusableCellWithIdentifier("profileImageTableViewCell", forIndexPath: indexPath) 94 | cell.textLabel?.text = imageListContentArray[indexPath.row] 95 | 96 | return cell 97 | } 98 | 99 | //MARK: - Tableview delegate 100 | 101 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 102 | let toPass = imageListContentArray[indexPath.row] 103 | delegate?.didSelectItem(toPass) 104 | } 105 | 106 | //MARK: - Private methods 107 | 108 | private func getImageListFromServer() { 109 | 110 | RESTEngine.sharedEngine.getImageListFromServerWithContactId(record.id, success: { response in 111 | self.imageListContentArray.removeAll() 112 | let records = response!["file"] as! JSONArray 113 | for record in records { 114 | if let record = record["name"] as? String { 115 | self.imageListContentArray.append(record) 116 | } 117 | } 118 | dispatch_async(dispatch_get_main_queue()) { 119 | self.tableView.reloadData() 120 | } 121 | 122 | }, failure: { error in 123 | // check if the error is file not found 124 | if error.code == 404 { 125 | let decode = error.userInfo["error"]?.firstItem as? JSON 126 | let message = decode?["message"] as? String 127 | if message != nil && message!.containsString("does not exist in storage") { 128 | NSLog("Warning: Error getting profile image list data from server: \(message)") 129 | return 130 | } 131 | } 132 | // else report normally 133 | NSLog("Error getting profile image list data from server: \(error)") 134 | dispatch_async(dispatch_get_main_queue()) { 135 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 136 | self.navigationController?.popToRootViewControllerAnimated(true) 137 | } 138 | }) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Controller/RegisterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegisterViewController.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RegisterViewController: UIViewController { 12 | 13 | @IBOutlet weak var emailTextField: UITextField! 14 | @IBOutlet weak var passwordTextField: UITextField! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | emailTextField.setValue(UIColor(red: 180/255.0, green: 180/255.0, blue: 180/255.0, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor") 20 | passwordTextField.setValue(UIColor(red: 180/255.0, green: 180/255.0, blue: 180/255.0, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor") 21 | } 22 | 23 | override func viewWillAppear(animated: Bool) { 24 | super.viewWillAppear(animated) 25 | 26 | let navBar = self.navBar 27 | navBar.showBackButton(true) 28 | navBar.showAddButton(false) 29 | navBar.showEditButton(false) 30 | } 31 | 32 | @IBAction func onSubmitClick(sender: AnyObject) { 33 | self.view.endEditing(true) 34 | if emailTextField.text?.characters.count > 0 && passwordTextField.text?.characters.count > 0 { 35 | 36 | RESTEngine.sharedEngine.registerWithEmail(emailTextField.text!, password: passwordTextField.text!, success: { response in 37 | RESTEngine.sharedEngine.sessionToken = response!["session_token"] as? String 38 | let defaults = NSUserDefaults.standardUserDefaults() 39 | defaults.setValue(self.emailTextField.text!, forKey: kUserEmail) 40 | defaults.setValue(self.passwordTextField.text!, forKey: kPassword) 41 | defaults.synchronize() 42 | 43 | dispatch_async(dispatch_get_main_queue()) { 44 | self.showAddressBookViewController() 45 | } 46 | }, failure: { error in 47 | NSLog("Error registering new user: \(error)") 48 | dispatch_async(dispatch_get_main_queue()) { 49 | Alert.showAlertWithMessage(error.errorMessage, fromViewController: self) 50 | } 51 | }) 52 | } else { 53 | let alert = UIAlertController(title: nil, message: "Enter email and password", preferredStyle: .Alert) 54 | alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) 55 | presentViewController(alert, animated: true, completion: nil) 56 | } 57 | } 58 | 59 | private func showAddressBookViewController() { 60 | let addressBookViewController = self.storyboard?.instantiateViewControllerWithIdentifier("AddressBookViewController") 61 | self.navigationController?.pushViewController(addressBookViewController!, animated: true) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/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 | APPL 17 | CFBundleShortVersionString 18 | 1.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Model/ContactDetailRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactDetailRecord.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | Contact info model 13 | */ 14 | class ContactDetailRecord { 15 | var id: NSNumber = NSNumber(integer: 0) 16 | var type: String! 17 | var phone: String! 18 | var email: String! 19 | var state: String! 20 | var zipCode: String! 21 | var country: String! 22 | var city: String! 23 | var address: String! 24 | var contactId: NSNumber! 25 | 26 | init() { 27 | } 28 | 29 | init(json: JSON) { 30 | id = json["id"] as! NSNumber 31 | address = json.nonNull("address") 32 | city = json.nonNull("city") 33 | country = json.nonNull("country") 34 | email = json.nonNull("email") 35 | state = json.nonNull("state") 36 | zipCode = json.nonNull("zip") 37 | type = json.nonNull("info_type") 38 | phone = json.nonNull("phone") 39 | contactId = json["contact_id"] as! NSNumber 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Model/ContactRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactRecord.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Dictionary { 12 | 13 | func nonNull(key: Key) -> String { 14 | let value = self[key] 15 | if value is NSNull { 16 | return "" 17 | } else { 18 | return value as! String 19 | } 20 | } 21 | } 22 | 23 | /** 24 | Contact model 25 | */ 26 | class ContactRecord: Equatable { 27 | var id: NSNumber! 28 | var firstName: String! 29 | var lastName: String! 30 | var notes: String! 31 | var skype: String! 32 | var twitter: String! 33 | var imageURL: String! 34 | 35 | var fullName: String { 36 | return "\(firstName) \(lastName)" 37 | } 38 | 39 | init() { 40 | 41 | } 42 | 43 | init(json: JSON) { 44 | id = json["id"] as! NSNumber 45 | firstName = json.nonNull("first_name") 46 | lastName = json.nonNull("last_name") 47 | 48 | if json["notes"] != nil { 49 | notes = json.nonNull("notes") 50 | } 51 | if json["skype"] != nil { 52 | skype = json.nonNull("skype") 53 | } 54 | if json["twitter"] != nil { 55 | twitter = json.nonNull("twitter") 56 | } 57 | if json["image_url"] != nil { 58 | imageURL = json.nonNull("image_url") 59 | } 60 | } 61 | } 62 | 63 | func ==(lhs: ContactRecord, rhs: ContactRecord) -> Bool { 64 | return lhs.id.isEqualToNumber(rhs.id) 65 | } 66 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Model/GroupRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupRecord.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias JSON = [String: AnyObject] 12 | typealias JSONArray = [JSON] 13 | 14 | /** 15 | Group model 16 | */ 17 | class GroupRecord { 18 | let id: NSNumber 19 | var name: String 20 | 21 | init(json: JSON) { 22 | id = json["id"] as! NSNumber 23 | name = json["name"] as! String 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Network/RESTEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RESTEngine.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/8/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let kAppVersion = "1.0.2" 12 | 13 | // change kApiKey and kBaseInstanceUrl to match your app and instance 14 | 15 | // API key for your app goes here. See README.md or https://github.com/dreamfactorysoftware/ios-swift-sdk 16 | private let kApiKey = "" 17 | private let kBaseInstanceUrl = "http://localhost:8080/api/v2" 18 | private let kSessionTokenKey = "SessionToken" 19 | private let kDbServiceName = "db/_table" 20 | private let kContainerName = "profile_images" 21 | 22 | let kUserEmail = "UserEmail" 23 | let kPassword = "UserPassword" 24 | 25 | typealias SuccessClosure = (JSON?) -> Void 26 | typealias ErrorClosure = (NSError) -> Void 27 | 28 | extension NSError { 29 | 30 | var errorMessage: String { 31 | if let errorMessage = self.userInfo["error"]?["message"] as? String { 32 | return errorMessage 33 | } 34 | return "Unknown error occurred" 35 | } 36 | } 37 | 38 | /** 39 | Routing to different type of API resources 40 | */ 41 | enum Routing { 42 | case User(resourseName: String) 43 | case Service(tableName: String) 44 | case ResourceFolder(folderPath: String) 45 | case ResourceFile(folderPath: String, fileName: String) 46 | 47 | var path: String { 48 | switch self { 49 | //rest path for request, form is /api/v2/user/resourceName 50 | case let .User(resourceName): 51 | return "\(kBaseInstanceUrl)/user/\(resourceName)" 52 | 53 | //rest path for request, form is /api/v2// 54 | case let .Service(tableName): 55 | return "\(kBaseInstanceUrl)/\(kDbServiceName)/\(tableName)" 56 | 57 | // rest path for request, form is /api/v2/files/container// 58 | case let .ResourceFolder(folderPath): 59 | return "\(kBaseInstanceUrl)/files/\(kContainerName)/\(folderPath)/" 60 | 61 | //rest path for request, form is /api/v2/files/container//filename 62 | case let .ResourceFile(folderPath, fileName): 63 | return "\(kBaseInstanceUrl)/files/\(kContainerName)/\(folderPath)/\(fileName)" 64 | } 65 | } 66 | } 67 | 68 | final class RESTEngine { 69 | static let sharedEngine = RESTEngine() 70 | 71 | var _sessionToken: String? 72 | var sessionToken: String? { 73 | get { 74 | if _sessionToken == nil { 75 | _sessionToken = NSUserDefaults.standardUserDefaults().valueForKey(kSessionTokenKey) as? String 76 | } 77 | return _sessionToken 78 | } 79 | set { 80 | if let value = newValue { 81 | NSUserDefaults.standardUserDefaults().setValue(value, forKey: kSessionTokenKey) 82 | NSUserDefaults.standardUserDefaults().synchronize() 83 | _sessionToken = value 84 | } else { 85 | NSUserDefaults.standardUserDefaults().removeObjectForKey(kSessionTokenKey) 86 | NSUserDefaults.standardUserDefaults().synchronize() 87 | _sessionToken = nil 88 | } 89 | } 90 | } 91 | 92 | let headerParams: [String: String] = { 93 | let dict = ["X-DreamFactory-Api-Key": kApiKey] 94 | return dict 95 | }() 96 | 97 | var sessionHeaderParams: [String: String] { 98 | var dict = headerParams 99 | dict["X-DreamFactory-Session-Token"] = sessionToken 100 | return dict 101 | } 102 | 103 | private let api = NIKApiInvoker.sharedInstance 104 | 105 | private init() { 106 | } 107 | func isConfigured() -> Bool { 108 | return kApiKey != "" 109 | } 110 | private func callApiWithPath(restApiPath: String, method: String, queryParams: [String: AnyObject]?, body: AnyObject?, headerParams: [String: String]?, success: SuccessClosure?, failure: ErrorClosure?) { 111 | api.restPath(restApiPath, method: method, queryParams: queryParams, body: body, headerParams: headerParams, contentType: "application/json", completionBlock: { (response, error) -> Void in 112 | if let error = error where failure != nil { 113 | failure!(error) 114 | } else if let success = success { 115 | success(response) 116 | } 117 | }) 118 | } 119 | 120 | // MARK: Helpers for POST/PUT/PATCH entity wrapping 121 | 122 | private func toResourceArray(entity:JSON) -> JSON { 123 | let jsonResource: JSON = ["resource" : [entity]] // DreamFactory REST API body with {"resource" = [ { record } ] } 124 | return jsonResource 125 | } 126 | private func toResourceArray(jsonArray:JSONArray) -> JSON { 127 | let jsonResource: JSON = ["resource" : jsonArray] // DreamFactory REST API body with {"resource" = [ { record } ] } 128 | return jsonResource 129 | } 130 | 131 | //MARK: - Authorization methods 132 | 133 | /** 134 | Sign in user 135 | */ 136 | func loginWithEmail(email: String, password: String, success: SuccessClosure, failure: ErrorClosure) { 137 | 138 | let requestBody: [String: AnyObject] = ["email": email, 139 | "password": password] 140 | 141 | callApiWithPath(Routing.User(resourseName: "session").path, method: "POST", queryParams: nil, body: requestBody, headerParams: headerParams, success: success, failure: failure) 142 | } 143 | 144 | /** 145 | Register new user 146 | */ 147 | func registerWithEmail(email: String, password: String, success: SuccessClosure, failure: ErrorClosure) { 148 | 149 | //login after signup 150 | let queryParams: [String: AnyObject] = ["login": "1"] 151 | let requestBody: [String: AnyObject] = ["email": email, 152 | "password": password, 153 | "first_name": "Address", 154 | "last_name": "Book", 155 | "name": "Address Book User"] 156 | 157 | callApiWithPath(Routing.User(resourseName: "register").path, method: "POST", queryParams: queryParams, body: requestBody, headerParams: headerParams, success: success, failure: failure) 158 | } 159 | 160 | //MARK: - Group methods 161 | 162 | /** 163 | Get all the groups from the database 164 | */ 165 | func getAddressBookContentFromServerWithSuccess(success: SuccessClosure, failure: ErrorClosure) { 166 | 167 | callApiWithPath(Routing.Service(tableName: "contact_group").path, method: "GET", queryParams: nil, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 168 | } 169 | 170 | private func removeContactGroupRelationsForGroupId(groupId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 171 | // remove all contact-group relations for the group being deleted 172 | 173 | // create filter to select all contact_group_relationship records that 174 | // reference the group being deleted 175 | let queryParams: [String: AnyObject] = ["filter": "contact_group_id=\(groupId)"] 176 | 177 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "DELETE", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 178 | } 179 | 180 | /** 181 | Remove group from server 182 | */ 183 | func removeGroupFromServerWithGroupId(groupId: NSNumber, success: SuccessClosure? = nil, failure: ErrorClosure) { 184 | // can not delete group until all references to it are removed 185 | // remove relations -> remove group 186 | // pass record ID so it knows what group we are removing 187 | 188 | removeContactGroupRelationsForGroupId(groupId, success: { _ in 189 | // delete the record by the record ID 190 | // form is "ids":"1,2,3" 191 | let queryParams: [String: AnyObject] = ["ids": "\(groupId)"] 192 | 193 | self.callApiWithPath(Routing.Service(tableName: "contact_group").path, method: "DELETE", queryParams: queryParams, body: nil, headerParams: self.sessionHeaderParams, success: success, failure: failure) 194 | 195 | }, failure: failure) 196 | } 197 | 198 | /** 199 | Add new group with name and contacts 200 | */ 201 | func addGroupToServerWithName(name: String, contactIds: [NSNumber]?, success: SuccessClosure, failure: ErrorClosure) { 202 | let requestBody = toResourceArray(["name": name]) 203 | 204 | callApiWithPath(Routing.Service(tableName: "contact_group").path, method: "POST", queryParams: nil, body: requestBody, headerParams: sessionHeaderParams, success: { response in 205 | // get the id of the new group, then add the relations 206 | let records = response!["resource"] as! JSONArray 207 | for recordInfo in records { 208 | self.addGroupContactRelationsForGroupWithId(recordInfo["id"] as! NSNumber, contactIds: contactIds, success: success, failure: failure) 209 | } 210 | }, failure: failure) 211 | } 212 | 213 | private func addGroupContactRelationsForGroupWithId(groupId: NSNumber, contactIds: [NSNumber]?, success: SuccessClosure, failure: ErrorClosure) { 214 | 215 | // if there are contacts to add skip server update 216 | if contactIds == nil || contactIds!.count == 0 { 217 | success(nil) 218 | return 219 | } 220 | 221 | // build request body 222 | /* 223 | * structure of request is: 224 | * { 225 | * "resource":[ 226 | * { 227 | * "contact_group_id":id, 228 | * "contact_id":id" 229 | * }, 230 | * {...} 231 | * ] 232 | * } 233 | */ 234 | var records: JSONArray = [] 235 | for contactId in contactIds! { 236 | records.append(["contact_group_id": groupId, 237 | "contact_id": contactId]) 238 | } 239 | 240 | let requestBody: [String: AnyObject] = ["resource": records] 241 | 242 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "POST", queryParams: nil, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 243 | } 244 | 245 | /** 246 | Update group with new name and contacts 247 | */ 248 | func updateGroupWithId(groupId: NSNumber, name: String, oldName: String, removedContactIds: [NSNumber]?, addedContactIds: [NSNumber]?, success: SuccessClosure, failure: ErrorClosure) { 249 | 250 | //if name didn't change skip server update 251 | if name == oldName { 252 | removeGroupContactRelationsForGroupWithId(groupId, contactIds: removedContactIds, success: { _ in 253 | self.addGroupContactRelationsForGroupWithId(groupId, contactIds: addedContactIds, success: success, failure: failure) 254 | }, failure: failure) 255 | return 256 | } 257 | 258 | // update name 259 | let queryParams: [String: AnyObject] = ["ids": groupId.stringValue] 260 | let requestBody = toResourceArray(["name": name]) 261 | 262 | callApiWithPath(Routing.Service(tableName: "contact_group").path, method: "PATCH", queryParams: queryParams, body: requestBody, headerParams: sessionHeaderParams, success: { _ in 263 | self.removeGroupContactRelationsForGroupWithId(groupId, contactIds: removedContactIds, success: { _ in 264 | self.addGroupContactRelationsForGroupWithId(groupId, contactIds: addedContactIds, success: success, failure: failure) 265 | }, failure: failure) 266 | }, failure: failure) 267 | } 268 | 269 | private func removeGroupContactRelationsForGroupWithId(groupId: NSNumber, contactIds: [NSNumber]?, success: SuccessClosure, failure: ErrorClosure) { 270 | 271 | // if there are no contacts to remove skip server update 272 | if contactIds == nil || contactIds!.count == 0 { 273 | success(nil) 274 | return 275 | } 276 | 277 | // remove contact-group relations 278 | 279 | // do not know the ID of the record to remove 280 | // one value for groupId, but many values for contactId 281 | // instead of making a long SQL query, change what we use as identifiers 282 | let queryParams: [String: AnyObject] = ["id_field": "contact_group_id,contact_id"] 283 | 284 | // build request body 285 | /* 286 | * structure of request is: 287 | * { 288 | * "resource":[ 289 | * { 290 | * "contact_group_id":id, 291 | * "contact_id":id" 292 | * }, 293 | * {...} 294 | * ] 295 | * } 296 | */ 297 | var records: JSONArray = [] 298 | for contactId in contactIds! { 299 | records.append(["contact_group_id": groupId, 300 | "contact_id": contactId]) 301 | } 302 | 303 | let requestBody: [String: AnyObject] = ["resource": records] 304 | 305 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "DELETE", queryParams: queryParams, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 306 | } 307 | 308 | //MARK: - Contact methods 309 | 310 | /** 311 | Get all the contacts from the database 312 | */ 313 | func getContactListFromServerWithSuccess(success: SuccessClosure, failure: ErrorClosure) { 314 | // only need to get the contactId and full contact name 315 | // set the fields param to give us just the fields we need 316 | let queryParams: [String: AnyObject] = ["fields": "id,first_name,last_name"] 317 | 318 | callApiWithPath(Routing.Service(tableName: "contact").path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 319 | } 320 | 321 | /** 322 | Get the list of contacts related to the group 323 | */ 324 | func getContactGroupRelationListFromServerWithGroupId(groupId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 325 | // create filter to get only the contact in the group 326 | let queryParams: [String: AnyObject] = ["filter": "contact_group_id=\(groupId)"] 327 | 328 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 329 | } 330 | 331 | /** 332 | Get all the contacts in the group using relational queries 333 | */ 334 | func getContactsListFromServerWithRelationWithGroupId(groupId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 335 | // only get contact_group_relationships for this group 336 | var queryParams: [String: AnyObject] = ["filter": "contact_group_id=\(groupId)"] 337 | 338 | // request without related would return just {id, groupId, contactId} 339 | // set the related field to go get the contact records referenced by 340 | // each contact_group_relationship record 341 | queryParams["related"] = "contact_by_contact_id" 342 | 343 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 344 | } 345 | 346 | /** 347 | Remove contact from server 348 | */ 349 | func removeContactWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 350 | // need to delete everything with references to contact before we can delete the contact 351 | // delete contact relation -> delete contact info -> delete profile images -> delete contact 352 | // remove contact by record ID 353 | 354 | removeContactRelationWithContactId(contactId, success: { _ in 355 | self.removeContactInfoWithContactId(contactId, success: { _ in 356 | self.removeContactImageFolderWithContactId(contactId, success: { _ in 357 | 358 | let queryParams: [String: AnyObject] = ["ids": "\(contactId)"] 359 | 360 | self.callApiWithPath(Routing.Service(tableName: "contact").path, method: "DELETE", queryParams: queryParams, body: nil, headerParams: self.sessionHeaderParams, success: success, failure: failure) 361 | 362 | }, failure: failure) 363 | }, failure: failure) 364 | }, failure: failure) 365 | } 366 | 367 | private func removeContactRelationWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 368 | // remove only contact-group relationships where contact is the contact to remove 369 | let queryParams: [String: AnyObject] = ["filter": "contact_id=\(contactId)"] 370 | 371 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "DELETE", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 372 | } 373 | 374 | private func removeContactInfoWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 375 | // remove only contact info for the contact we want to remove 376 | let queryParams: [String: AnyObject] = ["filter": "contact_id=\(contactId)"] 377 | 378 | callApiWithPath(Routing.Service(tableName: "contact_info").path, method: "DELETE", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 379 | } 380 | 381 | private func removeContactImageFolderWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 382 | 383 | // delete all files and folders in the target folder 384 | let queryParams: [String: AnyObject] = ["force": "1"] 385 | 386 | callApiWithPath(Routing.ResourceFolder(folderPath: "\(contactId)").path, method: "DELETE", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 387 | } 388 | 389 | /** 390 | Get contact info from server 391 | */ 392 | func getContactInfoFromServerWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 393 | 394 | // create filter to get info only for this contact 395 | let queryParams: [String: AnyObject] = ["filter": "contact_id=\(contactId)"] 396 | 397 | callApiWithPath(Routing.Service(tableName: "contact_info").path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 398 | } 399 | 400 | /** 401 | Get profile image for contact 402 | */ 403 | func getProfileImageFromServerWithContactId(contactId: NSNumber, fileName: String, success: SuccessClosure, failure: ErrorClosure) { 404 | 405 | // request a download from the file 406 | let queryParams: [String: AnyObject] = ["include_properties": "1", 407 | "content": "1", 408 | "download": "1"] 409 | 410 | callApiWithPath(Routing.ResourceFile(folderPath: "/\(contactId)", fileName: fileName).path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 411 | } 412 | 413 | /** 414 | Get all the group the contact is in using relational queries 415 | */ 416 | func getContactGroupsWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 417 | 418 | // only get contact_group_relationships for this contact 419 | var queryParams: [String: AnyObject] = ["filter": "contact_id=\(contactId)"] 420 | 421 | // request without related would return just {id, groupId, contactId} 422 | // set the related field to go get the group records referenced by 423 | // each contact_group_relationship record 424 | queryParams["related"] = "contact_group_by_contact_group_id" 425 | 426 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 427 | } 428 | 429 | func addContactToServerWithDetails(contactDetails: JSON, success: SuccessClosure, failure: ErrorClosure) { 430 | // need to create contact first, then can add contactInfo and group relationships 431 | 432 | let requestBody = toResourceArray(contactDetails) 433 | 434 | callApiWithPath(Routing.Service(tableName: "contact").path, method: "POST", queryParams: nil, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 435 | } 436 | 437 | func addContactGroupRelationToServerWithContactId(contactId: NSNumber, groupId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 438 | 439 | // build request body 440 | // need to put in any extra field-key pair and avoid NSUrl timeout issue 441 | // otherwise it drops connection 442 | let requestBody = toResourceArray(["contact_group_id": groupId, "contact_id": contactId]) 443 | 444 | callApiWithPath(Routing.Service(tableName: "contact_group_relationship").path, method: "POST", queryParams: nil, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 445 | } 446 | 447 | func addContactInfoToServer(info: JSONArray, success: SuccessClosure, failure: ErrorClosure) { 448 | 449 | let requestBody = toResourceArray(info) 450 | 451 | callApiWithPath(Routing.Service(tableName: "contact_info").path, method: "POST", queryParams: nil, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 452 | } 453 | 454 | /** 455 | Create contact image on server 456 | */ 457 | func addContactImageWithContactId(contactId: NSNumber, image: UIImage, imageName: String, success: SuccessClosure, failure: ErrorClosure) { 458 | 459 | // first we need to create folder, then image 460 | callApiWithPath(Routing.ResourceFolder(folderPath: "\(contactId)").path, method: "POST", queryParams: nil, body: nil, headerParams: sessionHeaderParams, success: { _ in 461 | 462 | self.putImageToFolderWithPath("\(contactId)", image: image, fileName: imageName, success: success, failure: failure) 463 | }, failure: failure) 464 | } 465 | 466 | func putImageToFolderWithPath(folderPath: String, image: UIImage, fileName: String, success: SuccessClosure, failure: ErrorClosure) { 467 | 468 | let imageData = UIImageJPEGRepresentation(image, 0.1) 469 | let file = NIKFile(name: fileName, mimeType: "application/octet-stream", data: imageData!) 470 | 471 | callApiWithPath(Routing.ResourceFile(folderPath: folderPath, fileName: fileName).path, method: "POST", queryParams: nil, body: file, headerParams: sessionHeaderParams, success: success, failure: failure) 472 | } 473 | 474 | 475 | /** 476 | Update an existing contact with the server 477 | */ 478 | func updateContactWithContactId(contactId: NSNumber, contactDetails: JSON, success: SuccessClosure, failure: ErrorClosure) { 479 | 480 | // set the id of the contact we are looking at 481 | let queryParams: [String: AnyObject] = ["ids": "\(contactId)"] 482 | let requestBody = toResourceArray(contactDetails) 483 | 484 | callApiWithPath(Routing.Service(tableName: "contact").path, method: "PATCH", queryParams: queryParams, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 485 | } 486 | 487 | /** 488 | Update an existing contact info with the server 489 | */ 490 | func updateContactInfo(info: JSONArray, success: SuccessClosure, failure: ErrorClosure) { 491 | 492 | let requestBody = toResourceArray(info) 493 | 494 | callApiWithPath(Routing.Service(tableName: "contact_info").path, method: "PATCH", queryParams: nil, body: requestBody, headerParams: sessionHeaderParams, success: success, failure: failure) 495 | } 496 | 497 | func getImageListFromServerWithContactId(contactId: NSNumber, success: SuccessClosure, failure: ErrorClosure) { 498 | 499 | // only want to get files, not any sub folders 500 | let queryParams: [String: AnyObject] = ["include_folders": "0", 501 | "include_files": "1"] 502 | 503 | callApiWithPath(Routing.ResourceFolder(folderPath: "\(contactId)").path, method: "GET", queryParams: queryParams, body: nil, headerParams: sessionHeaderParams, success: success, failure: failure) 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/Utils/UIUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIUtils.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/15/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Alert { 12 | 13 | static func showAlertWithMessage(message: String, fromViewController vc: UIViewController) { 14 | let alert = UIAlertView(title: nil, message: message, delegate: nil, cancelButtonTitle: "OK") 15 | alert.show() 16 | } 17 | } -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/View/ContactInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactInfoView.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private extension String { 12 | func isValidEmail() -> Bool { 13 | let emailRegex = "^.+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2}[A-Za-z]*$"; 14 | let emailTest = NSPredicate(format: "SELF MATCHES %@", argumentArray: [emailRegex]) 15 | return emailTest.evaluateWithObject(self) 16 | } 17 | } 18 | 19 | protocol ContactInfoDelegate: class { 20 | func onContactTypeClick(view: ContactInfoView, withTypes types:[String]) 21 | } 22 | 23 | class ContactInfoView: UIView, UITextFieldDelegate { 24 | var record: ContactDetailRecord! 25 | private var textFields: [String: UITextField]! 26 | private let contactTypes: [String] 27 | weak var delegate: ContactInfoDelegate! 28 | var contactType: String! { 29 | didSet { 30 | self.textFields["Type"]?.text = contactType 31 | } 32 | } 33 | 34 | override init(frame: CGRect) { 35 | self.contactTypes = ["work", "home", "mobile", "other"] 36 | super.init(frame: frame) 37 | 38 | buildContactTextFields(["Type", "Phone", "Email", "Address", "City", "State", "Zip", "Country"]) 39 | 40 | //resize 41 | var contectRect = CGRectZero 42 | for view in subviews { 43 | contectRect = CGRectUnion(contectRect, view.frame) 44 | } 45 | var oldFrame = self.frame 46 | oldFrame.size.height = contectRect.size.height 47 | self.frame = oldFrame 48 | } 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | func updateRecord() { 55 | record.type = textValueForKey("Type") 56 | record.phone = textValueForKey("Phone") 57 | record.email = textValueForKey("Email") 58 | record.address = textValueForKey("Address") 59 | record.city = textValueForKey("City") 60 | record.state = textValueForKey("State") 61 | record.zipCode = textValueForKey("Zip") 62 | record.country = textValueForKey("Country") 63 | } 64 | 65 | /** 66 | Validate email only 67 | other validations can be added, e.g. phone number, address 68 | */ 69 | func validateInfoWithResult(result: (Bool, String?) -> Void) { 70 | let email = textValueForKey("Email") 71 | if !email.isEmpty && !email.isValidEmail() { 72 | return result(false, "Not a valid email") 73 | } 74 | 75 | return result(true, nil) 76 | } 77 | 78 | func buildToDiciontary() -> [String: AnyObject] { 79 | var json = [ 80 | "contact_id": record.contactId, 81 | "info_type": record.type, 82 | "phone": record.phone, 83 | "email": record.email, 84 | "address": record.address, 85 | "city": record.city, 86 | "state": record.state, 87 | "zip": record.zipCode, 88 | "country": record.country] 89 | if record.id != 0 { 90 | json["id"] = record.id 91 | } 92 | return json 93 | } 94 | 95 | func updateFields() { 96 | putFieldIn(record.type, key: "Type") 97 | putFieldIn(record.phone, key: "Phone") 98 | putFieldIn(record.email, key: "Email") 99 | putFieldIn(record.address, key: "Address") 100 | putFieldIn(record.city, key: "City") 101 | putFieldIn(record.state, key: "State") 102 | putFieldIn(record.zipCode, key: "Zip") 103 | putFieldIn(record.country, key: "Country") 104 | 105 | reloadInputViews() 106 | setNeedsDisplay() 107 | } 108 | 109 | private func textValueForKey(key: String) -> String { 110 | return textFields[key]!.text! 111 | } 112 | 113 | private func putFieldIn(value: String, key: String) { 114 | if !value.isEmpty { 115 | textFields[key]?.text = value 116 | } else { 117 | textFields[key]?.text = "" 118 | } 119 | } 120 | 121 | private func buildContactTextFields(names: [String]) { 122 | var y: CGFloat = 0 123 | 124 | textFields = [:] 125 | 126 | for field in names { 127 | let textField = UITextField(frame: CGRectMake(frame.size.width * 0.05, y, frame.size.width * 0.9, 35)) 128 | textField.placeholder = field 129 | textField.font = UIFont(name: "Helvetica Neue", size: 20.0) 130 | textField.backgroundColor = UIColor.whiteColor() 131 | textField.layer.cornerRadius = 5 132 | addSubview(textField) 133 | 134 | textFields[field] = textField 135 | y += 40 136 | 137 | if field == "Type" { 138 | textField.enabled = false 139 | let button = UIButton(type: .System) 140 | button.frame = textField.frame 141 | button.setTitle("", forState: .Normal) 142 | button.backgroundColor = UIColor.clearColor() 143 | button.addTarget(self, action: #selector(onContactTypeClick), forControlEvents: .TouchDown) 144 | self.addSubview(button) 145 | textField.text = contactTypes[0] 146 | } 147 | } 148 | } 149 | 150 | func onContactTypeClick() { 151 | delegate.onContactTypeClick(self, withTypes: self.contactTypes) 152 | } 153 | 154 | func setTextFieldsDelegate(delegate: UITextFieldDelegate) { 155 | for textField in textFields.values { 156 | textField.delegate = delegate 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/View/CustomNavBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomNavBar.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/4/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CustomNavBar: UIToolbar { 12 | var backButton: UIButton! 13 | var addButton: UIButton! 14 | var editButton: UIButton! 15 | var doneButton: UIButton! 16 | 17 | private var isShowEdit = false 18 | private var isShowAdd = false 19 | private var isShowDone = false 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | self.backgroundColor = UIColor(red: 240/255.0, green: 240/255.0, blue: 240/255.0, alpha: 1.0) 24 | self.opaque = true 25 | self.setBackgroundImage(UIImage(named: "phone1"), forToolbarPosition: .Top, barMetrics: .Default) 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | func buildLogo() { 33 | let dfLogo = UIImage(named: "DreamFactory-logo-horiz-filled")! 34 | let resizable = dfLogo.resizableImageWithCapInsets(UIEdgeInsetsZero, resizingMode: .Stretch) 35 | let logoView = UIImageView(image: resizable) 36 | logoView.frame = CGRectMake(0, 0, frame.size.width * 0.45, dfLogo.size.height * dfLogo.size.width / (frame.size.width * 0.5)) 37 | logoView.contentMode = .ScaleAspectFit 38 | logoView.center = CGPointMake(frame.size.width * 0.48, frame.size.height * 0.6) 39 | addSubview(logoView) 40 | } 41 | 42 | func buildButtons() { 43 | backButton = UIButton(type: .System) 44 | backButton.frame = CGRectMake(frame.size.width * 0.1, 20, 50, 20) 45 | backButton.setTitleColor(UIColor(red: 216/255.0, green: 122/255.0, blue: 39/255.0, alpha: 1.0), forState: .Normal) 46 | backButton.titleLabel?.font = UIFont(name: "HelveticaNeue-Regular", size: 17.0) 47 | backButton.setTitle("Back", forState: .Normal) 48 | backButton.center = CGPointMake(frame.size.width * 0.1, 40) 49 | addSubview(backButton) 50 | showBackButton(false) 51 | 52 | addButton = UIButton(type: .System) 53 | addButton.frame = CGRectMake(frame.size.width * 0.85, 20, 50, 40) 54 | addButton.setTitleColor(UIColor(red: 107/255.0, green: 170/255.0, blue: 178/255.0, alpha: 1.0), forState: .Normal) 55 | addButton.titleLabel?.font = UIFont(name: "Helvetica Neue", size: 30.0) 56 | addButton.setTitle("+", forState: .Normal) 57 | addButton.center = CGPointMake(frame.size.width * 0.82, 38) 58 | addSubview(addButton) 59 | showAddButton(false) 60 | 61 | editButton = UIButton(type: .System) 62 | editButton.frame = CGRectMake(frame.size.width * 0.8, 20, 50, 40) 63 | editButton.setTitleColor(UIColor(red: 241/255.0, green: 141/255.0, blue: 42/255.0, alpha: 1.0), forState: .Normal) 64 | editButton.titleLabel?.font = UIFont(name: "HelveticaNeue-Regular", size: 17.0) 65 | editButton.setTitle("Edit", forState: .Normal) 66 | editButton.center = CGPointMake(frame.size.width * 0.93, 40) 67 | addSubview(editButton) 68 | showEditButton(false) 69 | 70 | doneButton = UIButton(type: .System) 71 | doneButton.frame = CGRectMake(frame.size.width * 0.8, 20, 50, 40) 72 | doneButton.setTitleColor(UIColor(red: 241/255.0, green: 141/255.0, blue: 42/255.0, alpha: 1.0), forState: .Normal) 73 | doneButton.titleLabel?.font = UIFont(name: "HelveticaNeue-Regular", size: 17.0) 74 | doneButton.setTitle("Done", forState: .Normal) 75 | doneButton.center = CGPointMake(frame.size.width * 0.93, 40) 76 | addSubview(doneButton) 77 | showDoneButton(false) 78 | } 79 | 80 | func showEditAndAdd() { 81 | showAddButton(true) 82 | showDoneButton(false) 83 | showEditButton(true) 84 | } 85 | 86 | func showAdd() { 87 | showAddButton(true) 88 | showDoneButton(false) 89 | showEditButton(false) 90 | } 91 | 92 | func showEdit() { 93 | showAddButton(false) 94 | showDoneButton(false) 95 | showEditButton(true) 96 | } 97 | 98 | func showDone() { 99 | showAddButton(false) 100 | showDoneButton(true) 101 | showEditButton(false) 102 | } 103 | 104 | func showEditButton(show: Bool) { 105 | editButton.hidden = !show 106 | } 107 | 108 | func showAddButton(show: Bool) { 109 | addButton.hidden = !show 110 | } 111 | 112 | func showBackButton(show: Bool) { 113 | backButton.hidden = !show 114 | } 115 | 116 | func showDoneButton(show: Bool) { 117 | doneButton.hidden = !show 118 | } 119 | 120 | func disableAllTouch() { 121 | userInteractionEnabled = false 122 | } 123 | 124 | func enableAllTouch() { 125 | userInteractionEnabled = true 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/View/PickerSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerSelector.swift 3 | // SampleAppSwift 4 | // 5 | // Created by Timur Umayev on 1/15/16. 6 | // Copyright © 2016 dreamfactory. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PickerSelectorDelegate: class { 12 | func pickerSelector(selector: PickerSelector, selectedValue value: String, index: Int) 13 | } 14 | 15 | class PickerSelector: UIViewController, UIPickerViewDataSource { 16 | @IBOutlet private weak var pickerView: UIPickerView! 17 | @IBOutlet private weak var cancelButton: UIBarButtonItem! 18 | @IBOutlet private weak var doneButton: UIBarButtonItem! 19 | @IBOutlet private weak var optionsToolBar: UIToolbar! 20 | 21 | var pickerData: [String] = [] 22 | weak var delegate: PickerSelectorDelegate! 23 | private var background: UIView! 24 | private var origin: CGPoint! 25 | 26 | convenience init() { 27 | self.init(nibName: "PickerSelector", bundle: NSBundle(forClass: self.dynamicType)) 28 | } 29 | 30 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { 31 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 32 | self.view.addSubview(pickerView) 33 | var frame = pickerView.frame 34 | frame.origin.y = CGRectGetMaxY(optionsToolBar.frame) 35 | pickerView.frame = frame 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | func showPickerOver(parent: UIViewController) { 43 | let window = UIApplication.sharedApplication().keyWindow! 44 | 45 | background = UIView(frame: window.bounds) 46 | background.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.0) 47 | window.addSubview(background) 48 | window.addSubview(self.view) 49 | parent.addChildViewController(self) 50 | 51 | var frame = self.view.frame 52 | let rotateAngle: CGFloat = 0.0 53 | let screenSize = CGSizeMake(window.frame.size.width, pickerSize.height) 54 | origin = CGPointMake(0, CGRectGetMaxY(window.bounds)) 55 | let target = CGPointMake(0, origin.y - CGRectGetHeight(frame)) 56 | 57 | self.view.transform = CGAffineTransformMakeRotation(rotateAngle) 58 | frame = self.view.frame 59 | frame.size = screenSize 60 | frame.origin = origin 61 | self.view.frame = frame 62 | 63 | UIView.animateWithDuration(0.3) { 64 | self.background.backgroundColor = self.background.backgroundColor?.colorWithAlphaComponent(0.5) 65 | frame = self.view.frame 66 | frame.origin = target 67 | self.view.frame = frame 68 | } 69 | pickerView.reloadAllComponents() 70 | } 71 | 72 | private var pickerSize: CGSize { 73 | var size = view.frame.size 74 | size.height = CGRectGetHeight(optionsToolBar.frame) + CGRectGetHeight(pickerView.frame) 75 | size.width = CGRectGetWidth(pickerView.frame) 76 | return size 77 | } 78 | 79 | @IBAction func setAction(sender: AnyObject) { 80 | if let delegate = delegate where pickerData.count > 0 { 81 | let index = pickerView.selectedRowInComponent(0) 82 | delegate.pickerSelector(self, selectedValue: pickerData[index], index: index) 83 | } 84 | dismissPicker() 85 | } 86 | 87 | @IBAction func cancelAction(sender: AnyObject) { 88 | dismissPicker() 89 | } 90 | 91 | private func dismissPicker() { 92 | UIView.animateWithDuration(0.3, animations: { 93 | self.background.backgroundColor = self.background.backgroundColor?.colorWithAlphaComponent(0.0) 94 | var frame = self.view.frame 95 | frame.origin = self.origin 96 | self.view.frame = frame 97 | }, completion: { _ in 98 | self.background.removeFromSuperview() 99 | self.view.removeFromSuperview() 100 | self.removeFromParentViewController() 101 | }) 102 | } 103 | 104 | //MARK: - Picker datasource 105 | 106 | func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { 107 | return 1 108 | } 109 | 110 | func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 111 | return pickerData.count 112 | } 113 | 114 | func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 115 | return pickerData[row] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /SampleAppSwift/SampleAppSwift/View/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 | -------------------------------------------------------------------------------- /SampleAppSwift/package/add_ios_swift.dfpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamfactorysoftware/ios-swift-sdk/a2ec340956adb5b3a3134881d9a9da777abdf7aa/SampleAppSwift/package/add_ios_swift.dfpkg -------------------------------------------------------------------------------- /SampleAppSwift/package/description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Address Book for iOS Swift", 3 | "description": "An address book app for iOS Swift showing user registration, user login, and CRUD.", 4 | "type": 0, 5 | "is_active": true 6 | } -------------------------------------------------------------------------------- /SampleAppSwift/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 | --------------------------------------------------------------------------------