Last active
January 30, 2017 21:42
-
-
Save mattpetters/ccf87678ccce0c354398 to your computer and use it in GitHub Desktop.
Corrected Twitter Package - CS193p Lecture 10 - Updated for XCode 6.3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MediaItem.swift | |
// | |
// Created by CS193p Instructor. | |
// Copyright (c) 2015 Stanford University. All rights reserved. | |
// | |
import Foundation | |
// holds the network url and aspectRatio of an image attached to a Tweet | |
// created automatically when a Tweet object is created | |
public struct MediaItem | |
{ | |
public let url: NSURL! | |
public let aspectRatio: Double | |
public var description: String { return (url.absoluteString ?? "no url") + " (aspect ratio = \(aspectRatio))" } | |
// MARK: - Private Implementation | |
init?(data: NSDictionary?) { | |
var valid = false | |
if let urlString = data?.valueForKeyPath(TwitterKey.MediaURL) as? NSString { | |
if let url = NSURL(string: urlString as String) { | |
self.url = url | |
let h = data?.valueForKeyPath(TwitterKey.Height) as? NSNumber | |
let w = data?.valueForKeyPath(TwitterKey.Width) as? NSNumber | |
if h != nil && w != nil && h?.doubleValue != 0 { | |
aspectRatio = w!.doubleValue / h!.doubleValue | |
return | |
} | |
} | |
} | |
return nil | |
} | |
struct TwitterKey { | |
static let MediaURL = "media_url_https" | |
static let Width = "sizes.small.w" | |
static let Height = "sizes.small.h" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Tweet.swift | |
// | |
// Created by CS193p Instructor. | |
// Copyright (c) 2015 Stanford University. All rights reserved. | |
// | |
import Foundation | |
// a simple container class which just holds the data in a Tweet | |
// IndexedKeywords are substrings of the Tweet's text | |
// for example, a hashtag or other user or url that is mentioned in the Tweet | |
// note carefully the comments on the two range properties in an IndexedKeyword | |
// Tweet instances re created by fetching from Twitter using a TwitterRequest | |
public class Tweet : Printable | |
{ | |
public var text: String | |
public var user: User | |
public let created: NSDate | |
public let id: String? | |
public var media = [MediaItem]() | |
public var hashtags = [IndexedKeyword]() | |
public var urls = [IndexedKeyword]() | |
public var userMentions = [IndexedKeyword]() | |
public struct IndexedKeyword: Printable | |
{ | |
public var keyword: String // will include # or @ or http:// prefix | |
public var range: Range<String.Index> // index into the Tweet's text property only | |
public var nsrange: NSRange = NSRange() // index into an NS[Attributed]String made from the Tweet's text | |
public init?(data: NSDictionary?, inText: String, prefix: String?) { | |
let indices = data?.valueForKeyPath(TwitterKey.Entities.Indices) as? NSArray | |
if let startIndex = (indices?.firstObject as? NSNumber)?.integerValue { | |
if let endIndex = (indices?.lastObject as? NSNumber)?.integerValue { | |
let length = count(inText) | |
if length > 0 { | |
let start = max(min(startIndex, length-1), 0) | |
let end = max(min(endIndex, length), 0) | |
if end > start { | |
range = advance(inText.startIndex, start)...advance(inText.startIndex, end-1) | |
keyword = inText.substringWithRange(range) | |
if prefix != nil && !keyword.hasPrefix(prefix!) && start > 0 { | |
range = advance(inText.startIndex, start-1)...advance(inText.startIndex, end-2) | |
keyword = inText.substringWithRange(range) | |
} | |
if prefix == nil || keyword.hasPrefix(prefix!) { | |
nsrange = inText.rangeOfString(keyword, nearRange: NSMakeRange(startIndex, endIndex-startIndex)) | |
if nsrange.location != NSNotFound { | |
return | |
} | |
} | |
} | |
} | |
} | |
} | |
return nil | |
} | |
public var description: String { get { return "\(keyword) (\(nsrange.location), \(nsrange.location+nsrange.length-1))" } } | |
} | |
public var description: String { return "\(user) - \(created)\n\(text)\nhashtags: \(hashtags)\nurls: \(urls)\nuser_mentions: \(userMentions)" + (id == nil ? "" : "\nid: \(id!)") } | |
// MARK: - Private Implementation | |
init?(data: NSDictionary?) { | |
if let user = User(data: data?.valueForKeyPath(TwitterKey.User) as? NSDictionary) { | |
self.user = user | |
if let text = data?.valueForKeyPath(TwitterKey.Text) as? String { | |
self.text = text | |
if let created = (data?.valueForKeyPath(TwitterKey.Created) as? String)?.asTwitterDate { | |
self.created = created | |
id = data?.valueForKeyPath(TwitterKey.ID) as? String | |
if let mediaEntities = data?.valueForKeyPath(TwitterKey.Media) as? NSArray { | |
for mediaData in mediaEntities { | |
if let mediaItem = MediaItem(data: mediaData as? NSDictionary) { | |
media.append(mediaItem) | |
} | |
} | |
} | |
let hashtagMentionsArray = data?.valueForKeyPath(TwitterKey.Entities.Hashtags) as? NSArray | |
hashtags = getIndexedKeywords(hashtagMentionsArray, inText: text, prefix: "#") | |
let urlMentionsArray = data?.valueForKeyPath(TwitterKey.Entities.URLs) as? NSArray | |
urls = getIndexedKeywords(urlMentionsArray, inText: text, prefix: "h") | |
let userMentionsArray = data?.valueForKeyPath(TwitterKey.Entities.UserMentions) as? NSArray | |
userMentions = getIndexedKeywords(userMentionsArray, inText: text, prefix: "@") | |
return | |
} | |
} | |
} | |
// we've failed | |
// but compiler won't let us out of here with non-optional values unset | |
// so set them to anything just to able to return nil | |
// we could make these implicitly-unwrapped optionals, but they should never be nil, ever | |
self.text = "" | |
self.user = User() | |
self.created = NSDate() | |
self.id = "" | |
return nil | |
} | |
private func getIndexedKeywords(dictionary: NSArray?, inText: String, prefix: String? = nil) -> [IndexedKeyword] { | |
var results = [IndexedKeyword]() | |
if let indexedKeywords = dictionary { | |
for indexedKeywordData in indexedKeywords { | |
if let indexedKeyword = IndexedKeyword(data: indexedKeywordData as? NSDictionary, inText: inText, prefix: prefix) { | |
results.append(indexedKeyword) | |
} | |
} | |
} | |
return results | |
} | |
struct TwitterKey { | |
static let User = "user" | |
static let Text = "text" | |
static let Created = "created_at" | |
static let ID = "id_str" | |
static let Media = "entities.media" | |
struct Entities { | |
static let Hashtags = "entities.hashtags" | |
static let URLs = "entities.urls" | |
static let UserMentions = "entities.user_mentions" | |
static let Indices = "indices" | |
} | |
} | |
} | |
private extension NSString { | |
func rangeOfString(substring: NSString, nearRange: NSRange) -> NSRange { | |
var start = max(min(nearRange.location, length-1), 0) | |
var end = max(min(nearRange.location + nearRange.length, length), 0) | |
var done = false | |
while !done { | |
let range = rangeOfString(substring as String, options: NSStringCompareOptions.allZeros, range: NSMakeRange(start, end-start)) | |
if range.location != NSNotFound { | |
return range | |
} | |
done = true | |
if start > 0 { start-- ; done = false } | |
if end < length { end++ ; done = false } | |
} | |
return NSMakeRange(NSNotFound, 0) | |
} | |
} | |
private extension String { | |
var asTwitterDate: NSDate? { | |
get { | |
let dateFormatter = NSDateFormatter() | |
dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" | |
return dateFormatter.dateFromString(self) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// TwitterRequest.swift | |
// | |
// Created by CS193p Instructor. | |
// Copyright (c) 2015 Stanford University. All rights reserved. | |
// | |
import Foundation | |
import Accounts | |
import Social | |
import CoreLocation | |
// Simple Twitter query class | |
// Create an instance of it using one of the initializers | |
// Set the requestType and parameters (if not using a convenience init that sets those) | |
// Call fetch (or fetchTweets if fetching Tweets) | |
// The handler passed in will be called when the information comes back from Twitter | |
// Once a successful fetch has happened, | |
// a follow-on TwitterRequest to get more Tweets (newer or older) can be created | |
// using the requestFor{Newer,Older} methods | |
private var twitterAccount: ACAccount? | |
public class TwitterRequest | |
{ | |
public let requestType: String | |
public var parameters = Dictionary<String, String>() | |
// designated initializer | |
public init(_ requestType: String, _ parameters: Dictionary<String, String> = [:]) { | |
self.requestType = requestType | |
self.parameters = parameters | |
} | |
// convenience initializer for creating a TwitterRequest that is a search for Tweets | |
public convenience init(search: String, count: Int = 0, _ resultType: SearchResultType = .Mixed, _ region: CLCircularRegion? = nil) { | |
var parameters = [TwitterKey.Query : search] | |
if count > 0 { | |
parameters[TwitterKey.Count] = "\(count)" | |
} | |
switch resultType { | |
case .Recent: parameters[TwitterKey.ResultType] = TwitterKey.ResultTypeRecent | |
case .Popular: parameters[TwitterKey.ResultType] = TwitterKey.ResultTypePopular | |
default: break | |
} | |
if let geocode = region { | |
parameters[TwitterKey.Geocode] = "\(geocode.center.latitude),\(geocode.center.longitude),\(geocode.radius/1000.0)km" | |
} | |
self.init(TwitterKey.SearchForTweets, parameters) | |
} | |
public enum SearchResultType { | |
case Mixed | |
case Recent | |
case Popular | |
} | |
// convenience "fetch" for when self is a request that returns Tweet(s) | |
// handler is not necessarily invoked on the main queue | |
public func fetchTweets(handler: ([Tweet]) -> Void) { | |
fetch { results in | |
var tweets = [Tweet]() | |
var tweetArray: NSArray? | |
if let dictionary = results as? NSDictionary { | |
if let tweets = dictionary[TwitterKey.Tweets] as? NSArray { | |
tweetArray = tweets | |
} else if let tweet = Tweet(data: dictionary) { | |
tweets = [tweet] | |
} | |
} else if let array = results as? NSArray { | |
tweetArray = array | |
} | |
if tweetArray != nil { | |
for tweetData in tweetArray! { | |
if let tweet = Tweet(data: tweetData as? NSDictionary) { | |
tweets.append(tweet) | |
} | |
} | |
} | |
handler(tweets) | |
} | |
} | |
public typealias PropertyList = AnyObject | |
// send an arbitrary request off to Twitter | |
// calls the handler (not necessarily on the main queue) | |
// with the JSON results converted to a Property List | |
public func fetch(handler: (results: PropertyList?) -> Void) { | |
performTwitterRequest(SLRequestMethod.GET, handler: handler) | |
} | |
// generates a request for older Tweets than were returned by self | |
// only makes sense if self has done a fetch already | |
// only makes sense for requests for Tweets | |
public var requestForOlder: TwitterRequest? { | |
return min_id != nil ? modifiedRequest(parametersToChange: [TwitterKey.MaxID : min_id!]) : nil | |
} | |
// generates a request for newer Tweets than were returned by self | |
// only makes sense if self has done a fetch already | |
// only makes sense for requests for Tweets | |
public var requestForNewer: TwitterRequest? { | |
return (max_id != nil) ? modifiedRequest(parametersToChange: [TwitterKey.SinceID : max_id!], clearCount: true) : nil | |
} | |
// MARK: - Private Implementation | |
// creates an appropriate SLRequest using the specified SLRequestMethod | |
// then calls the other version of this method that takes an SLRequest | |
// handler is not necessarily called on the main queue | |
func performTwitterRequest(method: SLRequestMethod, handler: (PropertyList?) -> Void) { | |
var jsonExtension = (self.requestType.rangeOfString(JSONExtension) == nil) ? JSONExtension : "" | |
let request = SLRequest( | |
forServiceType: SLServiceTypeTwitter, | |
requestMethod: method, | |
URL: NSURL(string: "\(TwitterURLPrefix)\(self.requestType)\(jsonExtension)"), | |
parameters: self.parameters | |
) | |
performTwitterRequest(request, handler: handler) | |
} | |
// sends the request to Twitter | |
// unpackages the JSON response into a Property List | |
// and calls handler (not necessarily on the main queue) | |
func performTwitterRequest(request: SLRequest, handler: (PropertyList?) -> Void) { | |
if let account = twitterAccount { | |
request.account = account | |
request.performRequestWithHandler { (jsonResponse, httpResponse, _) in | |
var propertyListResponse: PropertyList? | |
if jsonResponse != nil { | |
propertyListResponse = NSJSONSerialization.JSONObjectWithData( | |
jsonResponse, | |
options: NSJSONReadingOptions.MutableLeaves, | |
error: nil | |
) | |
if propertyListResponse == nil { | |
let error = "Couldn't parse JSON response." | |
self.log(error) | |
propertyListResponse = error | |
} | |
} else { | |
let error = "No response from Twitter." | |
self.log(error) | |
propertyListResponse = error | |
} | |
self.synchronize { | |
self.captureFollowonRequestInfo(propertyListResponse) | |
} | |
handler(propertyListResponse) | |
} | |
} else { | |
let accountStore = ACAccountStore() | |
let twitterAccountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter) | |
accountStore.requestAccessToAccountsWithType(twitterAccountType, options: nil) { (granted, _) in | |
if granted { | |
if let account = accountStore.accountsWithAccountType(twitterAccountType)?.last as? ACAccount { | |
twitterAccount = account | |
self.performTwitterRequest(request, handler: handler) | |
} else { | |
let error = "Couldn't discover Twitter account type." | |
self.log(error) | |
handler(error) | |
} | |
} else { | |
let error = "Access to Twitter was not granted." | |
self.log(error) | |
handler(error) | |
} | |
} | |
} | |
} | |
private var min_id: String? = nil | |
private var max_id: String? = nil | |
// modifies parameters in an existing request to create a new one | |
private func modifiedRequest(#parametersToChange: Dictionary<String,String>, clearCount: Bool = false) -> TwitterRequest { | |
var newParameters = parameters | |
for (key, value) in parametersToChange { | |
newParameters[key] = value | |
} | |
if clearCount { newParameters[TwitterKey.Count] = nil } | |
return TwitterRequest(requestType, newParameters) | |
} | |
// captures the min_id and max_id information | |
// to support requestForNewer and requestForOlder | |
private func captureFollowonRequestInfo(propertyListResponse: PropertyList?) { | |
if let responseDictionary = propertyListResponse as? NSDictionary { | |
self.max_id = responseDictionary.valueForKeyPath(TwitterKey.SearchMetadata.MaxID) as? String | |
if let next_results = responseDictionary.valueForKeyPath(TwitterKey.SearchMetadata.NextResults) as? String { | |
for queryTerm in next_results.componentsSeparatedByString(TwitterKey.SearchMetadata.Separator) { | |
if queryTerm.hasPrefix("?\(TwitterKey.MaxID)=") { | |
let next_id = queryTerm.componentsSeparatedByString("=") | |
if next_id.count == 2 { | |
self.min_id = next_id[1] | |
} | |
} | |
} | |
} | |
} | |
} | |
// debug println with identifying prefix | |
private func log(whatToLog: AnyObject) { | |
debugPrintln("TwitterRequest: \(whatToLog)") | |
} | |
// synchronizes access to self across multiple threads | |
private func synchronize(closure: () -> Void) { | |
objc_sync_enter(self) | |
closure() | |
objc_sync_exit(self) | |
} | |
// constants | |
let JSONExtension = ".json" | |
let TwitterURLPrefix = "https://api.twitter.com/1.1/" | |
// keys in Twitter responses/queries | |
struct TwitterKey { | |
static let Count = "count" | |
static let Query = "q" | |
static let Tweets = "statuses" | |
static let ResultType = "result_type" | |
static let ResultTypeRecent = "recent" | |
static let ResultTypePopular = "popular" | |
static let Geocode = "geocode" | |
static let SearchForTweets = "search/tweets" | |
static let MaxID = "max_id" | |
static let SinceID = "since_id" | |
struct SearchMetadata { | |
static let MaxID = "search_metadata.max_id_str" | |
static let NextResults = "search_metadata.next_results" | |
static let Separator = "&" | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// User.swift | |
// | |
// Created by CS193p Instructor. | |
// Copyright (c) 2015 Stanford University. All rights reserved. | |
// | |
import Foundation | |
// container to hold data about a Twitter user | |
public struct User: Printable | |
{ | |
public let screenName: String | |
public let name: String | |
public var profileImageURL: NSURL? | |
public var verified: Bool = false | |
public var id: String! | |
public var description: String { var v = verified ? " ✅" : ""; return "@\(screenName) (\(name))\(v)" } | |
// MARK: - Private Implementation | |
init?(data: NSDictionary?) { | |
let name = data?.valueForKeyPath(TwitterKey.Name) as? String | |
let screenName = data?.valueForKeyPath(TwitterKey.ScreenName) as? String | |
if name != nil && screenName != nil { | |
self.name = name! | |
self.screenName = screenName! | |
self.id = data?.valueForKeyPath(TwitterKey.ID) as? String | |
if let verified = data?.valueForKeyPath(TwitterKey.Verified)?.boolValue { | |
self.verified = verified | |
} | |
if let urlString = data?.valueForKeyPath(TwitterKey.ProfileImageURL) as? String { | |
self.profileImageURL = NSURL(string: urlString) | |
} | |
} else { | |
return nil | |
} | |
} | |
var asPropertyList: AnyObject { | |
var dictionary = Dictionary<String,String>() | |
dictionary[TwitterKey.Name] = self.name | |
dictionary[TwitterKey.ScreenName] = self.screenName | |
dictionary[TwitterKey.ID] = self.id | |
dictionary[TwitterKey.Verified] = verified ? "YES" : "NO" | |
dictionary[TwitterKey.ProfileImageURL] = profileImageURL?.absoluteString | |
return dictionary | |
} | |
init() { | |
screenName = "Unknown" | |
name = "Unknown" | |
} | |
struct TwitterKey { | |
static let Name = "name" | |
static let ScreenName = "screen_name" | |
static let ID = "id_str" | |
static let Verified = "verified" | |
static let ProfileImageURL = "profile_image_url" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey! Could you please update to Xcode 7.2 swift 2.1?