Skip to content

Instantly share code, notes, and snippets.

@ajwaxman
Created January 12, 2016 14:14
Show Gist options
  • Save ajwaxman/9c48c0b4c622bffb879f to your computer and use it in GitHub Desktop.
Save ajwaxman/9c48c0b4c622bffb879f to your computer and use it in GitHub Desktop.
Updated Twitter Package - CS193p Lecture 10 - Updated for Swift 2.0 and Xcode 7.2
//
// MediaItem.swift
// Twitter
//
// 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 var url: NSURL!
public var aspectRatio: Double = 0
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
valid = true
}
}
}
if !valid {
return nil
}
}
struct TwitterKey {
static let MediaURL = "media_url_https"
static let Width = "sizes.small.w"
static let Height = "sizes.small.h"
}
}
//
// Tweet.swift
// Twitter
//
// 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 : CustomStringConvertible
{
public var text: String
public var user: User
public var created: NSDate
public var id: String?
public var media = [MediaItem]()
public var mediaMentions = [IndexedKeyword]()
public var hashtags = [IndexedKeyword]()
public var urls = [IndexedKeyword]()
public var userMentions = [IndexedKeyword]()
public struct IndexedKeyword: CustomStringConvertible
{
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 = NSMakeRange(0, 0) // 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 = inText.characters.count
if length > 0 {
let start = max(min(startIndex, length-1), 0)
let end = max(min(endIndex, length), 0)
if end > start {
range = inText.startIndex.advancedBy(start)...inText.startIndex.advancedBy(end-1)
keyword = inText.substringWithRange(range)
if prefix != nil && !keyword.hasPrefix(prefix!) && start > 0 {
range = inText.startIndex.advancedBy(start-1)...inText.startIndex.advancedBy(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)
}
}
mediaMentions = getIndexedKeywords(mediaEntities, inText: text, prefix: "h")
}
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()
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(), 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.locale = NSLocale(localeIdentifier: "en_US")
dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy"
return dateFormatter.dateFromString(self)
}
}
}
//
// TwitterRequest.swift
// Twitter
//
// 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 var 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) {
let 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?
do {
if jsonResponse != nil {
propertyListResponse = try NSJSONSerialization.JSONObjectWithData(
jsonResponse,
options: NSJSONReadingOptions.MutableLeaves
)
}
} catch let error as NSError {
print(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 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) {
debugPrint("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 = "&"
}
}
}
//
// User.swift
// Twitter
//
// 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: CustomStringConvertible
{
public var screenName: String
public var name: String
public var profileImageURL: NSURL?
public var verified: Bool = false
public var id: String!
public var description: String { let 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