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