├── TwitterJSON.playground
├── timeline.xctimeline
├── playground.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── amitbijlani.xcuserdatad
│ │ ├── UserInterfaceState.xcuserstate
│ │ └── WorkspaceSettings.xcsettings
├── contents.xcplayground
├── Contents.swift
└── Resources
│ └── Timeline.json
└── README.md
/TwitterJSON.playground/timeline.xctimeline:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/TwitterJSON.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TwitterJSON.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/TwitterJSON.playground/playground.xcworkspace/xcuserdata/amitbijlani.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abijlani/json-swift/HEAD/TwitterJSON.playground/playground.xcworkspace/xcuserdata/amitbijlani.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # json-swift
2 | An exploration into monads, functors and applicatives apply to parse JSON using Swift. This code example was presented at CocoaConf DC 2015. You can also download the accompanying [slide deck](https://speakerdeck.com/abijlani/json-plus-swift-functionally-beautiful).
3 |
--------------------------------------------------------------------------------
/TwitterJSON.playground/playground.xcworkspace/xcuserdata/amitbijlani.xcuserdatad/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildLocationStyle
6 | UseAppPreferences
7 | CustomBuildLocationType
8 | RelativeToDerivedData
9 | DerivedDataLocationStyle
10 | Default
11 | IssueFilterStyle
12 | ShowActiveSchemeOnly
13 | LiveSourceIssuesEnabled
14 |
15 | SnapshotAutomaticallyBeforeSignificantChanges
16 |
17 | SnapshotLocationStyle
18 | Default
19 |
20 |
21 |
--------------------------------------------------------------------------------
/TwitterJSON.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | /*:
2 | # JSON + Swift: Functionally Beautiful
3 | ## Amit Bijlani
4 | ### @paradoxed
5 | */
6 |
7 | import UIKit
8 | import XCPlayground
9 |
10 | XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: false)
11 |
12 | let fileName = "Timeline"
13 | let fileExtension = "json"
14 |
15 | /*:
16 | ## Type aliases
17 | */
18 |
19 | typealias JSON = AnyObject
20 | typealias JSONDictionary = Dictionary
21 | typealias JSONArray = Array
22 |
23 |
24 | /*:
25 | ## The Either Type
26 | ### Either an error or a valid value which has to be stored in a generic container defined below as Box
27 | */
28 |
29 | enum Result {
30 | case Error(NSError)
31 | case Value(Box)
32 | }
33 |
34 |
35 | final class Box {
36 | let value: A
37 |
38 | init(_ value: A) {
39 | self.value = value
40 | }
41 | }
42 |
43 |
44 | /*:
45 | ## Monad Bind Operator
46 | ### Binds an Optional to a Function that takes a non-optional and returns an Optional
47 | */
48 |
49 |
50 | infix operator >>> { associativity left precedence 150 }
51 |
52 | func >>>(a: A?, f: A -> B?) -> B? {
53 | if let x = a {
54 | return f(x)
55 | } else {
56 | return .None
57 | }
58 | }
59 |
60 |
61 | /*:
62 | ## Functors `fmap`
63 | ### Applying functions to values wrapped in an Optional
64 | */
65 |
66 |
67 | infix operator <^> { associativity left } // Functor's fmap (usually <$>)
68 |
69 | func <^>(f: A -> B, a: A?) -> B? {
70 | if let x = a {
71 | return f(x)
72 | } else {
73 | return .None
74 | }
75 | }
76 |
77 | /*:
78 | ## Applicatives functors `apply`
79 | ### Applying wrapped functions to values wrapped in an Optional
80 | */
81 |
82 | infix operator <*> { associativity left } // Applicative's apply
83 |
84 | func <*>(f: (A -> B)?, a: A?) -> B? {
85 | if let x = a {
86 | if let fx = f {
87 | return fx(x)
88 | }
89 | }
90 | return .None
91 | }
92 |
93 | /*:
94 | ## Casting Functions
95 | */
96 |
97 | func JSONToString(object: JSON) -> String? {
98 | return object as? String
99 | }
100 |
101 | func JSONToInt(object: JSON) -> Int? {
102 | return object as? Int
103 | }
104 |
105 | func JSONToDictionary(object: JSON) -> JSONDictionary? {
106 | return object as? JSONDictionary
107 | }
108 |
109 | func JSONToArray(object: JSON) -> JSONArray? {
110 | return object as? JSONArray
111 | }
112 |
113 | /*:
114 | ## User model
115 | ### Note the currying function named create
116 | */
117 |
118 | struct User {
119 | let name: String
120 | let profileDescription: String
121 | let followersCount: Int
122 |
123 | static func create(name: String)(profileDescription: String)(followersCount: Int) -> User {
124 | return User(name: name, profileDescription: profileDescription, followersCount: followersCount)
125 | }
126 |
127 | }
128 |
129 | /*:
130 | ## parseData
131 | ### Takes a data parameter and the second parameter is a callback function that returns a Result enum.
132 | */
133 |
134 | func parseData(data: NSData?, callback: (Result) -> ()) {
135 | var jsonErrorOptional: NSError?
136 | let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers, error: &jsonErrorOptional)
137 |
138 | if let err = jsonErrorOptional {
139 | callback(.Error(err))
140 | return
141 | }
142 |
143 | if let statuses = jsonObject >>> JSONToArray,
144 | aStatus = statuses[0] >>> JSONToDictionary,
145 | userDictionary = aStatus["user"] >>> JSONToDictionary {
146 |
147 | let user = User.create <^>
148 | userDictionary["name"] >>> JSONToString <*>
149 | userDictionary["description"] >>> JSONToString <*>
150 | userDictionary["followers_count"] >>> JSONToInt
151 |
152 | if let u = user {
153 | callback(.Value(Box(u)))
154 | return
155 | }
156 | }
157 |
158 | // if all else fails return error
159 | callback(.Error(NSError()))
160 | }
161 |
162 | /*:
163 | ## Calling `parseData`
164 | */
165 |
166 | //read
167 | let bundle = NSBundle.mainBundle()
168 | let data = NSData(contentsOfURL: bundle.URLForResource(fileName, withExtension: fileExtension)!)
169 |
170 | parseData(data){result in
171 | switch result {
172 | case let .Error(err):
173 | print("Error \(err)")
174 |
175 | case let .Value(username):
176 | print(username.value.name)
177 | }
178 | }
179 |
180 |
181 |
--------------------------------------------------------------------------------
/TwitterJSON.playground/Resources/Timeline.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "coordinates": null,
4 | "truncated": false,
5 | "created_at": "Tue Aug 28 21:16:23 +0000 2012",
6 | "favorited": false,
7 | "id_str": "240558470661799936",
8 | "in_reply_to_user_id_str": null,
9 | "entities": {
10 | "urls": [],
11 | "hashtags": [],
12 | "user_mentions": []
13 | },
14 | "text": "just another test",
15 | "contributors": null,
16 | "id": 240558470661799940,
17 | "retweet_count": 0,
18 | "in_reply_to_status_id_str": null,
19 | "geo": null,
20 | "retweeted": false,
21 | "in_reply_to_user_id": null,
22 | "place": null,
23 | "source": "OAuthDancerReborn",
24 | "user": {
25 | "name": "oauthDancer",
26 | "profile_sidebar_fill_color": "DDEEF6",
27 | "profile_background_tile": true,
28 | "profile_sidebar_border_color": "C0DEED",
29 | "profile_image_url": "http: //a0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg",
30 | "created_at": "WedMar0319: 37: 35+00002010",
31 | "location": "SanFrancisco, CA",
32 | "follow_request_sent": false,
33 | "id_str": "119476949",
34 | "is_translator": false,
35 | "profile_link_color": "0084B4",
36 | "entities": {
37 | "url": {
38 | "urls": [
39 | {
40 | "expanded_url": null,
41 | "url": "http: //bit.ly/oauth-dancer",
42 | "indices": [
43 | 0,
44 | 26
45 | ],
46 | "display_url": null
47 | }
48 | ]
49 | },
50 | "description": null
51 | },
52 | "default_profile": false,
53 | "url": "http: //bit.ly/oauth-dancer",
54 | "contributors_enabled": false,
55 | "favourites_count": 7,
56 | "utc_offset": null,
57 | "profile_image_url_https": "https: //si0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg",
58 | "id": 119476949,
59 | "listed_count": 1,
60 | "profile_use_background_image": true,
61 | "profile_text_color": "333333",
62 | "followers_count": 28,
63 | "lang": "en",
64 | "protected": false,
65 | "geo_enabled": true,
66 | "notifications": false,
67 | "description": "",
68 | "profile_background_color": "C0DEED",
69 | "verified": false,
70 | "time_zone": null,
71 | "profile_background_image_url_https": "https: //si0.twimg.com/profile_background_images/80151733/oauth-dance.png",
72 | "statuses_count": 166,
73 | "profile_background_image_url": "http: //a0.twimg.com/profile_background_images/80151733/oauth-dance.png",
74 | "default_profile_image": false,
75 | "friends_count": 14,
76 | "following": false,
77 | "show_all_inline_media": false,
78 | "screen_name": "oauth_dancer"
79 | },
80 | "in_reply_to_screen_name": null,
81 | "in_reply_to_status_id": null
82 | },
83 | {
84 | "coordinates": {
85 | "coordinates": [
86 | -122.25831,
87 | 37.871609
88 | ],
89 | "type": "Point"
90 | },
91 | "truncated": false,
92 | "created_at": "TueAug2821: 08: 15+00002012",
93 | "favorited": false,
94 | "id_str": "240556426106372096",
95 | "in_reply_to_user_id_str": null,
96 | "entities": {
97 | "urls": [
98 | {
99 | "expanded_url": "http: //blogs.ischool.berkeley.edu/i290-abdt-s12/",
100 | "url": "http: //t.co/bfj7zkDJ",
101 | "indices": [
102 | 79,
103 | 99
104 | ],
105 | "display_url": "blogs.ischool.berkeley.edu/i290-abdt-s12/"
106 | }
107 | ],
108 | "hashtags": [],
109 | "user_mentions": [
110 | {
111 | "name": "Cal",
112 | "id_str": "17445752",
113 | "id": 17445752,
114 | "indices": [
115 | 60,
116 | 64
117 | ],
118 | "screen_name": "Cal"
119 | },
120 | {
121 | "name": "OthmanLaraki",
122 | "id_str": "20495814",
123 | "id": 20495814,
124 | "indices": [
125 | 70,
126 | 77
127 | ],
128 | "screen_name": "othman"
129 | }
130 | ]
131 | },
132 | "text": "lecturingatthe\"analyzingbigdatawithtwitter\"classat@calwith@othmanhttp: //t.co/bfj7zkDJ",
133 | "contributors": null,
134 | "id": 240556426106372100,
135 | "retweet_count": 3,
136 | "in_reply_to_status_id_str": null,
137 | "geo": {
138 | "coordinates": [
139 | 37.871609,
140 | -122.25831
141 | ],
142 | "type": "Point"
143 | },
144 | "retweeted": false,
145 | "possibly_sensitive": false,
146 | "in_reply_to_user_id": null,
147 | "place": {
148 | "name": "Berkeley",
149 | "country_code": "US",
150 | "country": "UnitedStates",
151 | "attributes": {},
152 | "url": "http: //api.twitter.com/1/geo/id/5ef5b7f391e30aff.json",
153 | "id": "5ef5b7f391e30aff",
154 | "bounding_box": {
155 | "coordinates": [
156 | [
157 | [
158 | -122.367781,
159 | 37.835727
160 | ],
161 | [
162 | -122.234185,
163 | 37.835727
164 | ],
165 | [
166 | -122.234185,
167 | 37.905824
168 | ],
169 | [
170 | -122.367781,
171 | 37.905824
172 | ]
173 | ]
174 | ],
175 | "type": "Polygon"
176 | },
177 | "full_name": "Berkeley, CA",
178 | "place_type": "city"
179 | },
180 | "source": "SafarioniOS",
181 | "user": {
182 | "name": "RaffiKrikorian",
183 | "profile_sidebar_fill_color": "DDEEF6",
184 | "profile_background_tile": false,
185 | "profile_sidebar_border_color": "C0DEED",
186 | "profile_image_url": "http: //a0.twimg.com/profile_images/1270234259/raffi-headshot-casual_normal.png",
187 | "created_at": "SunAug1914: 24: 06+00002007",
188 | "location": "SanFrancisco, California",
189 | "follow_request_sent": false,
190 | "id_str": "8285392",
191 | "is_translator": false,
192 | "profile_link_color": "0084B4",
193 | "entities": {
194 | "url": {
195 | "urls": [
196 | {
197 | "expanded_url": "http: //about.me/raffi.krikorian",
198 | "url": "http: //t.co/eNmnM6q",
199 | "indices": [
200 | 0,
201 | 19
202 | ],
203 | "display_url": "about.me/raffi.krikorian"
204 | }
205 | ]
206 | },
207 | "description": {
208 | "urls": []
209 | }
210 | },
211 | "default_profile": true,
212 | "url": "http: //t.co/eNmnM6q",
213 | "contributors_enabled": false,
214 | "favourites_count": 724,
215 | "utc_offset": -28800,
216 | "profile_image_url_https": "https: //si0.twimg.com/profile_images/1270234259/raffi-headshot-casual_normal.png",
217 | "id": 8285392,
218 | "listed_count": 619,
219 | "profile_use_background_image": true,
220 | "profile_text_color": "333333",
221 | "followers_count": 18752,
222 | "lang": "en",
223 | "protected": false,
224 | "geo_enabled": true,
225 | "notifications": false,
226 | "description": "Directorof@twittereng'sPlatformServices.Ibreakthings.",
227 | "profile_background_color": "C0DEED",
228 | "verified": false,
229 | "time_zone": "PacificTime(US&Canada)",
230 | "profile_background_image_url_https": "https: //si0.twimg.com/images/themes/theme1/bg.png",
231 | "statuses_count": 5007,
232 | "profile_background_image_url": "http: //a0.twimg.com/images/themes/theme1/bg.png",
233 | "default_profile_image": false,
234 | "friends_count": 701,
235 | "following": true,
236 | "show_all_inline_media": true,
237 | "screen_name": "raffi"
238 | },
239 | "in_reply_to_screen_name": null,
240 | "in_reply_to_status_id": null
241 | },
242 | {
243 | "coordinates": null,
244 | "truncated": false,
245 | "created_at": "TueAug2819: 59: 34+00002012",
246 | "favorited": false,
247 | "id_str": "240539141056638977",
248 | "in_reply_to_user_id_str": null,
249 | "entities": {
250 | "urls": [],
251 | "hashtags": [],
252 | "user_mentions": []
253 | },
254 | "text": "You'dberightmoreoftenifyouthoughtyouwerewrong.",
255 | "contributors": null,
256 | "id": 240539141056638980,
257 | "retweet_count": 1,
258 | "in_reply_to_status_id_str": null,
259 | "geo": null,
260 | "retweeted": false,
261 | "in_reply_to_user_id": null,
262 | "place": null,
263 | "source": "web",
264 | "user": {
265 | "name": "TaylorSingletary",
266 | "profile_sidebar_fill_color": "FBFBFB",
267 | "profile_background_tile": true,
268 | "profile_sidebar_border_color": "000000",
269 | "profile_image_url": "http: //a0.twimg.com/profile_images/2546730059/f6a8zq58mg1hn0ha8vie_normal.jpeg",
270 | "created_at": "WedMar0722: 23: 19+00002007",
271 | "location": "SanFrancisco, CA",
272 | "follow_request_sent": false,
273 | "id_str": "819797",
274 | "is_translator": false,
275 | "profile_link_color": "c71818",
276 | "entities": {
277 | "url": {
278 | "urls": [
279 | {
280 | "expanded_url": "http: //www.rebelmouse.com/episod/",
281 | "url": "http: //t.co/Lxw7upbN",
282 | "indices": [
283 | 0,
284 | 20
285 | ],
286 | "display_url": "rebelmouse.com/episod/"
287 | }
288 | ]
289 | },
290 | "description": {
291 | "urls": []
292 | }
293 | },
294 | "default_profile": false,
295 | "url": "http: //t.co/Lxw7upbN",
296 | "contributors_enabled": false,
297 | "favourites_count": 15990,
298 | "utc_offset": -28800,
299 | "profile_image_url_https": "https: //si0.twimg.com/profile_images/2546730059/f6a8zq58mg1hn0ha8vie_normal.jpeg",
300 | "id": 819797,
301 | "listed_count": 340,
302 | "profile_use_background_image": true,
303 | "profile_text_color": "D20909",
304 | "followers_count": 7126,
305 | "lang": "en",
306 | "protected": false,
307 | "geo_enabled": true,
308 | "notifications": false,
309 | "description": "RealityTechnician,TwitterAPIteam,synthesizerenthusiast;amostexcellentadventureintimelines.Iknowit'shardtobelieveinsomethingyoucan'tsee.",
310 | "profile_background_color": "000000",
311 | "verified": false,
312 | "time_zone": "PacificTime(US&Canada)",
313 | "profile_background_image_url_https": "https: //si0.twimg.com/profile_background_images/643655842/hzfv12wini4q60zzrthg.png",
314 | "statuses_count": 18076,
315 | "profile_background_image_url": "http: //a0.twimg.com/profile_background_images/643655842/hzfv12wini4q60zzrthg.png",
316 | "default_profile_image": false,
317 | "friends_count": 5444,
318 | "following": true,
319 | "show_all_inline_media": true,
320 | "screen_name": "episod"
321 | },
322 | "in_reply_to_screen_name": null,
323 | "in_reply_to_status_id": null
324 | }
325 | ]
--------------------------------------------------------------------------------