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