diff --git a/.gitignore b/.gitignore index d522f94..5cd9fa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,30 @@ -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? -# -# Pods/ +# Xcode +.DS_Store +build/ +*.dll +*.a +*.approj +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.xcworkspace +!default.xcworkspace +*/xcuserdata +profile +*.moved-aside +DerivedData +.idea/ +.sconsign.dblite +Scripts/cache +Scripts/json +Scripts/venv +Carthage/Checkouts +Carthage/Build + +SwiftTweetGettr/TwitterCredentials.plist diff --git a/README.md b/README.md index 43b6aa1..2ebacb5 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,11 @@ SwiftTweetGettr New to iOS or Swift? This tiny twitter client project will hopefully get you started building your own apps or seeing common iOS patterns in Swift. You'll need to replace the API Key and API Secret in the Client class with those of your own. You can get them by getting a Twitter developer account and creating an app. Just go here: https://apps.twitter.com/app/new + +# Updated + +Updated for Swift 3.1 / Xcode 8.3.3+ by [@sarah_j_smith](https://github.com/sarah-j-smith) + +You can put the credentials into the PLIST file `SwiftTweetGettr/TwitterCredentials.plist` and they'll be read from there (so you don't need to embed them in the code if you don't want to). Handy if you're going to be displaying the code in a group setting. :-) + +Handy git tip: `git update-index --assume-unchanged SwiftTweetGetter/TwitterCredentials.plist` will ignore your changes so you don't push secrets to your repo, if you wind up forking this project. diff --git a/SwiftTweetGettr.xcodeproj/project.pbxproj b/SwiftTweetGettr.xcodeproj/project.pbxproj index 069d66b..a023dea 100644 --- a/SwiftTweetGettr.xcodeproj/project.pbxproj +++ b/SwiftTweetGettr.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 63FEA3D6193E888700400515 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FEA3D5193E888700400515 /* ViewController.swift */; }; 63FEA3D9193E888700400515 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63FEA3D7193E888700400515 /* Main.storyboard */; }; 63FEA3DB193E888700400515 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63FEA3DA193E888700400515 /* Images.xcassets */; }; + F6E571E01F6CF253008DF6C3 /* TwitterCredentials.plist in Resources */ = {isa = PBXBuildFile; fileRef = F6E571DF1F6CF253008DF6C3 /* TwitterCredentials.plist */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,6 +33,7 @@ 63FEA3D5193E888700400515 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 63FEA3D8193E888700400515 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63FEA3DA193E888700400515 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + F6E571DF1F6CF253008DF6C3 /* TwitterCredentials.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TwitterCredentials.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,6 +84,7 @@ 63FEA3D1193E888700400515 /* Supporting Files */ = { isa = PBXGroup; children = ( + F6E571DF1F6CF253008DF6C3 /* TwitterCredentials.plist */, 63FEA3D2193E888700400515 /* Info.plist */, ); name = "Supporting Files"; @@ -113,11 +116,13 @@ 63FEA3C6193E888700400515 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0600; + LastUpgradeCheck = 0830; ORGANIZATIONNAME = "Jeff Menter"; TargetAttributes = { 63FEA3CD193E888700400515 = { CreatedOnToolsVersion = 6.0; + DevelopmentTeam = 6KYAUVEW7R; + LastSwiftMigration = 0830; }; }; }; @@ -146,6 +151,7 @@ files = ( 63FEA3D9193E888700400515 /* Main.storyboard in Resources */, 63FEA3DB193E888700400515 /* Images.xcassets in Resources */, + F6E571E01F6CF253008DF6C3 /* TwitterCredentials.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -194,15 +200,19 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -215,7 +225,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; METAL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -236,8 +246,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -245,15 +257,17 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; METAL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; @@ -263,10 +277,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = 6KYAUVEW7R; INFOPLIST_FILE = SwiftTweetGettr/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = au.com.smithsoft.SwiftTweetGettr; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -275,10 +292,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = 6KYAUVEW7R; INFOPLIST_FILE = SwiftTweetGettr/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = au.com.smithsoft.SwiftTweetGettr; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/SwiftTweetGettr/AppDelegate.swift b/SwiftTweetGettr/AppDelegate.swift index fa6b2cd..2019381 100644 --- a/SwiftTweetGettr/AppDelegate.swift +++ b/SwiftTweetGettr/AppDelegate.swift @@ -6,11 +6,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let greenHue:CGFloat = 123.0/360.0 self.window?.tintColor = UIColor(hue: greenHue, saturation: 0.5, brightness: 0.5, alpha: 1.0) UINavigationBar.appearance().barTintColor = UIColor(hue: greenHue, saturation: 0.05, brightness: 0.95, alpha: 1.0) return true } -} \ No newline at end of file +} diff --git a/SwiftTweetGettr/Base.lproj/Main.storyboard b/SwiftTweetGettr/Base.lproj/Main.storyboard index 0d03a6c..f8fec72 100644 --- a/SwiftTweetGettr/Base.lproj/Main.storyboard +++ b/SwiftTweetGettr/Base.lproj/Main.storyboard @@ -1,21 +1,28 @@ - - + + + + + - - + + + + + + + - + - - - + + @@ -23,42 +30,55 @@ - - - - + + + + + + - + - @@ -77,18 +97,19 @@ line 3 - - + + + + + + + + + + + + @@ -109,9 +130,9 @@ line 3 - + - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. @@ -144,6 +165,6 @@ line 3 - + diff --git a/SwiftTweetGettr/Extensions.swift b/SwiftTweetGettr/Extensions.swift index 1451d35..ec8f312 100644 --- a/SwiftTweetGettr/Extensions.swift +++ b/SwiftTweetGettr/Extensions.swift @@ -1,94 +1,85 @@ import UIKit -extension NSURLResponse { +extension URLResponse { func isHTTPResponseValid() -> Bool { - if let response = self as? NSHTTPURLResponse { + if let response = self as? HTTPURLResponse { return (response.statusCode >= 200 && response.statusCode <= 299) } return false } } -extension NSData { +extension Data { func json() -> AnyObject { - return NSJSONSerialization.JSONObjectWithData(self, options: nil, error: nil)! + let result = try? JSONSerialization.jsonObject(with: self, options: []) + return result! as AnyObject } } extension UITableView { - func scrollToTop(#animated: Bool) + func scrollToTop(animated: Bool) { - scrollRectToVisible(CGRectMake(0, 0, 1, 0), animated: true) + scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 0), animated: true) } } extension UIViewController { - func showAlertViewWithMessage(message : String) + func showAlertViewWithMessage(_ message : String) { - var alertController = UIAlertController(title: "Oops!", message: message, preferredStyle: UIAlertControllerStyle.Alert) - alertController.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) - presentViewController(alertController, animated: true, completion: nil) + let alertController = UIAlertController(title: "Oops!", message: message, preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + present(alertController, animated: true, completion: nil) } } extension String { - func data() -> NSData + func data() -> Data { - return dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! + return self.data(using: String.Encoding.utf8, allowLossyConversion: false)! } func base64Encoded() -> String { - return data().base64EncodedStringWithOptions(nil) + let dataFromString = self.data() + return dataFromString.base64EncodedString(options: []) } - func createURL() -> NSURL + func createURL() -> URL { - return NSURL(string: self)! + let secureURL = self.replacingOccurrences(of: "http://", with: "https://") + return URL(string: secureURL)! } func stringByRemovingWhitespace() -> String { - let trimmed = self.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) - return trimmed.stringByReplacingOccurrencesOfString(" ", withString: "", options: nil, range: nil) - } -} - -extension NSMutableURLRequest { - - class func getRequestWithURL(url:NSURL) -> NSMutableURLRequest - { - var request = NSMutableURLRequest(URL: url) - request.HTTPMethod = "GET" - return request - } - - class func postRequestWithURL(url:NSURL, body:String) -> NSMutableURLRequest - { - var request = NSMutableURLRequest(URL: url) - request.HTTPMethod = "POST" - request.HTTPBody = body.data() - return request + let trimmed = self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + return trimmed.replacingOccurrences(of: " ", with:"", options: [], range: nil) } } extension UIImageView { - func loadURL(url:String) { - NSURLConnection.sendAsynchronousRequest(NSURLRequest(URL: NSURL(string: url)!), queue: NSOperationQueue.mainQueue()) { (response, data, error) -> Void in - if response.isHTTPResponseValid() { - if let image = UIImage(data: data) { - self.image = image + func loadURL(_ url:String) { + + let request = URLRequest(url: url.createURL()) + let task = URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in + let validResponse = response?.isHTTPResponseValid() ?? false + if validResponse { + DispatchQueue.main.async { [unowned self] in + if let image = UIImage(data: data!) { + self.image = image + } } } } + task.resume() } -} \ No newline at end of file +} diff --git a/SwiftTweetGettr/Images.xcassets/AppIcon.appiconset/Contents.json b/SwiftTweetGettr/Images.xcassets/AppIcon.appiconset/Contents.json index 33ec0bc..b8236c6 100644 --- a/SwiftTweetGettr/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/SwiftTweetGettr/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,15 +1,35 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "60x60", diff --git a/SwiftTweetGettr/Info.plist b/SwiftTweetGettr/Info.plist index ef7c0da..744d1cd 100644 --- a/SwiftTweetGettr/Info.plist +++ b/SwiftTweetGettr/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.yournamehere.${PRODUCT_NAME:rfc1034identifier} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -30,5 +30,9 @@ armv7 + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + diff --git a/SwiftTweetGettr/Tweet.swift b/SwiftTweetGettr/Tweet.swift index 5bc201e..4b891c0 100644 --- a/SwiftTweetGettr/Tweet.swift +++ b/SwiftTweetGettr/Tweet.swift @@ -3,12 +3,12 @@ import UIKit class Tweet { - class func tweetsFromArray(from: Array>) -> Array + class func tweetsFromArray(_ from: Array>) -> Array { return from.map( { Tweet(tweetDictionary: $0) } ) } - private let tweetDictionary:Dictionary + fileprivate let tweetDictionary:Dictionary var text:String { return tweetDictionary["text"] as! String } var createdAt:String { return tweetDictionary["created_at"] as! String } @@ -19,13 +19,13 @@ class Tweet { var profileImageURL:String? { return user()["profile_image_url"] as? String } var biggerProfileImageURL:String? { if let url = user()["profile_image_url"] as? String { - return url.stringByReplacingOccurrencesOfString("_normal", withString: "_bigger") + return url.replacingOccurrences(of: "_normal", with: "_bigger") } return nil } var originalProfileImageURL:String? { if let url = user()["profile_image_url"] as? String { - return url.stringByReplacingOccurrencesOfString("_normal", withString: "") + return url.replacingOccurrences(of: "_normal", with: "") } return nil } @@ -38,9 +38,9 @@ class Tweet { }) } - private func user() -> Dictionary + fileprivate func user() -> Dictionary { return tweetDictionary["user"] as! Dictionary } -} \ No newline at end of file +} diff --git a/SwiftTweetGettr/TweetCell.swift b/SwiftTweetGettr/TweetCell.swift index 1e5ec1a..bd85f66 100644 --- a/SwiftTweetGettr/TweetCell.swift +++ b/SwiftTweetGettr/TweetCell.swift @@ -8,7 +8,7 @@ class TweetCell : UITableViewCell { @IBOutlet weak var handleTextField: UILabel! @IBOutlet weak var statusTextField: UILabel! - func applyTweet(tweet:Tweet) -> Void + func applyTweet(_ tweet:Tweet) -> Void { nameTextField.text = tweet.name handleTextField.text = tweet.screenName @@ -16,4 +16,4 @@ class TweetCell : UITableViewCell { userImageView.image = tweet.userImage userImageView.loadURL(tweet.biggerProfileImageURL!) } -} \ No newline at end of file +} diff --git a/SwiftTweetGettr/TweetsTableViewDelegate.swift b/SwiftTweetGettr/TweetsTableViewDelegate.swift index f0b3085..9bbd93e 100644 --- a/SwiftTweetGettr/TweetsTableViewDelegate.swift +++ b/SwiftTweetGettr/TweetsTableViewDelegate.swift @@ -5,14 +5,14 @@ class TweetsTableViewDelegate : NSObject, UITableViewDataSource, UITableViewDele var tweets = Array() - func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tweets.count } - func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCellWithIdentifier("tweetCell", forIndexPath: indexPath) as! TweetCell + let cell = tableView.dequeueReusableCell(withIdentifier: "tweetCell", for: indexPath) as! TweetCell let tweet = tweets[indexPath.row] cell.applyTweet(tweet) @@ -20,4 +20,4 @@ class TweetsTableViewDelegate : NSObject, UITableViewDataSource, UITableViewDele return cell } -} \ No newline at end of file +} diff --git a/SwiftTweetGettr/TwitterAuthorization.swift b/SwiftTweetGettr/TwitterAuthorization.swift index 78ed9c2..2f50b83 100644 --- a/SwiftTweetGettr/TwitterAuthorization.swift +++ b/SwiftTweetGettr/TwitterAuthorization.swift @@ -7,22 +7,22 @@ class TwitterAuthorization { class func token() -> String? { - return NSUserDefaults.standardUserDefaults().stringForKey(kAuthorizationTokenStorageKey) + return UserDefaults.standard.string(forKey: kAuthorizationTokenStorageKey) } - class func setToken(token:String?) -> Void + class func setToken(_ token:String?) -> Void { - if let actuallyToken = token { - NSUserDefaults.standardUserDefaults().setObject(token, forKey: kAuthorizationTokenStorageKey) + if token != nil { + UserDefaults.standard.set(token, forKey: kAuthorizationTokenStorageKey) } else { - NSUserDefaults.standardUserDefaults().removeObjectForKey(kAuthorizationTokenStorageKey) + UserDefaults.standard.removeObject(forKey: kAuthorizationTokenStorageKey) } - NSUserDefaults.standardUserDefaults().synchronize() + UserDefaults.standard.synchronize() } class func hasToken() -> Bool { - if let actuallyToken = token() { + if token() != nil { return true } return false diff --git a/SwiftTweetGettr/TwitterClient.swift b/SwiftTweetGettr/TwitterClient.swift index 5d30e08..eafe0ed 100644 --- a/SwiftTweetGettr/TwitterClient.swift +++ b/SwiftTweetGettr/TwitterClient.swift @@ -14,15 +14,23 @@ private let kAuthorizationContentType = "application/x-www-form-urlencoded;chars class TwitterClient { - class func fetchAuthorizationToken(#success:() -> Void, failure:(String) -> Void) + class func fetchAuthorizationToken(success:@escaping () -> Void, _ failure:@escaping (String) -> Void) { - var tokenRequest = NSMutableURLRequest.postRequestWithURL(kOAuthRootURL.createURL(), body: kAuthorizationBody) + if headerForAuthorization().isEmpty + { + failure("API Key & Secret not configured!") + return + } + var tokenRequest = URLRequest(url: kOAuthRootURL.createURL()) + tokenRequest.httpBody = kAuthorizationBody.data() tokenRequest.addValue(kAuthorizationContentType, forHTTPHeaderField: kContentTypeHeader) tokenRequest.addValue(headerForAuthorization(), forHTTPHeaderField: kAuthorizationHeaderKey) - - NSURLConnection.sendAsynchronousRequest(tokenRequest, queue: NSOperationQueue.mainQueue(), completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in - if response.isHTTPResponseValid() { - TwitterAuthorization.setToken(data.json()["access_token"] as? String) + tokenRequest.httpMethod = "POST" + + let task = URLSession.shared.dataTask(with: tokenRequest) { (data: Data?, response: URLResponse?, error: Error?) -> Void in + let validResponse = response?.isHTTPResponseValid() ?? false + if validResponse { + TwitterAuthorization.setToken(data?.json()["access_token"] as? String) if TwitterAuthorization.hasToken() { success() } else { @@ -31,38 +39,46 @@ class TwitterClient { } else { self.handleFailure(failure, error: error, response: response) } - }) + } + task.resume() } - class func fetchTweetsForUser(userName:String, success:(Array) -> Void, failure:(String) -> Void) + class func fetchTweetsForUser(_ userName:String, success:@escaping (Array) -> Void, failure:@escaping (String) -> Void) { - var tweetRequest = NSMutableURLRequest.getRequestWithURL((kTimelineRootURL + userName.stringByRemovingWhitespace()).createURL()) + let userURL = kTimelineRootURL + userName.stringByRemovingWhitespace() + var tweetRequest = URLRequest(url: userURL.createURL()) + tweetRequest.httpMethod = "GET" tweetRequest.addValue(headerWithAuthorization(), forHTTPHeaderField: kAuthorizationHeaderKey) - - NSURLConnection.sendAsynchronousRequest(tweetRequest, queue: NSOperationQueue.mainQueue(), completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in - if response.isHTTPResponseValid() { - if let results:Array> = data.json() as? Array { + + let task = URLSession.shared.dataTask(with: tweetRequest) { (data: Data?, response: URLResponse?, error: Error?) -> Void in + let validResponse = response?.isHTTPResponseValid() ?? false + if validResponse { + if let results:Array> = data?.json() as? Array { success(Tweet.tweetsFromArray(results)) } } else { self.handleFailure(failure, error: error, response: response) } - }) + } + task.resume() } - class func fetchImageAtURL(url:String, success:(UIImage?) -> Void) -> Void + class func fetchImageAtURL(_ url:String, success:@escaping (UIImage?) -> Void) -> Void { - NSURLConnection.sendAsynchronousRequest(NSURLRequest(URL: NSURL(string: url)!), queue: NSOperationQueue.mainQueue()) { (response, data, error) -> Void in - if response.isHTTPResponseValid() { - success(UIImage(data: data)) + let imageURLRequest = URLRequest(url: url.createURL()) + let task = URLSession.shared.dataTask(with: imageURLRequest) { (data, response, error) -> Void in + let validResponse = response?.isHTTPResponseValid() ?? false + if validResponse { + success(UIImage(data: data!)) } } + task.resume() } - private class func handleFailure(failure:(String) -> Void, error:NSError!, response: NSURLResponse!) -> Void + fileprivate class func handleFailure(_ failure:(String) -> Void, error:Error!, response: URLResponse!) -> Void { if let actuallyError = error { - failure(actuallyError.description) + failure(actuallyError.localizedDescription) } else if let actuallyResponse = response { failure(actuallyResponse.description) } else { @@ -70,12 +86,17 @@ class TwitterClient { } } - private class func headerForAuthorization() -> String + fileprivate class func headerForAuthorization() -> String { - return "Basic " + (kAPIKey + ":" + kAPISecret).base64Encoded() + let credentialsPath = Bundle.main.path(forResource: "TwitterCredentials", ofType: "plist") + let twitterCredentials = NSDictionary(contentsOfFile: credentialsPath!) + let apiKey = twitterCredentials?["APIKey"] as? String ?? kAPIKey + let apiSecret = twitterCredentials?["APISecret"] as? String ?? kAPISecret + if apiKey.isEmpty || apiSecret.isEmpty { return "" } + return "Basic " + (apiKey + ":" + apiSecret).base64Encoded() } - private class func headerWithAuthorization() -> String + fileprivate class func headerWithAuthorization() -> String { return "Bearer " + TwitterAuthorization.token()! } diff --git a/SwiftTweetGettr/TwitterCredentials.plist b/SwiftTweetGettr/TwitterCredentials.plist new file mode 100644 index 0000000..334c045 --- /dev/null +++ b/SwiftTweetGettr/TwitterCredentials.plist @@ -0,0 +1,10 @@ + + + + + APISecret + + APIKey + + + diff --git a/SwiftTweetGettr/ViewController.swift b/SwiftTweetGettr/ViewController.swift index c4fda4c..1ead4c3 100644 --- a/SwiftTweetGettr/ViewController.swift +++ b/SwiftTweetGettr/ViewController.swift @@ -3,7 +3,7 @@ import UIKit class ViewController : UIViewController, UITextFieldDelegate { - var spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray) + var spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray) var tweetsTableViewDelegate = TweetsTableViewDelegate() @IBOutlet var textField : UITextField! @@ -15,50 +15,50 @@ class ViewController : UIViewController, UITextFieldDelegate { super.viewDidLoad() textField.rightView = spinner - textField.rightViewMode = .Always + textField.rightViewMode = .always button.layer.cornerRadius = 5 button.layer.borderWidth = 1 - button.layer.borderColor = button.titleLabel?.textColor.CGColor + button.layer.borderColor = button.titleLabel?.textColor.cgColor tableView.dataSource = tweetsTableViewDelegate tableView.delegate = tweetsTableViewDelegate } - override func viewDidAppear(animated: Bool) + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if let selected = tableView.indexPathForSelectedRow() { - tableView.deselectRowAtIndexPath(tableView.indexPathForSelectedRow()!, animated: animated) + if tableView.indexPathForSelectedRow != nil { + tableView.deselectRow(at: tableView.indexPathForSelectedRow!, animated: animated) } - if textField.text.isEmpty { + if (textField.text?.isEmpty)! { textField.becomeFirstResponder() } } - override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - let index = tableView.indexPathForSelectedRow()?.row + let index = tableView.indexPathForSelectedRow?.row let tweet = tweetsTableViewDelegate.tweets[index!] - (segue.destinationViewController.view as! UITextView).text = tweet.description + (segue.destination.view as! UITextView).text = tweet.description } - func textFieldShouldReturn(textField: UITextField) -> Bool + func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if !textField.text.isEmpty { + if !(textField.text?.isEmpty)! { buttonWasTapped(nil) } return textField.resignFirstResponder() } - @IBAction func textFieldDidChange(sender : AnyObject) + @IBAction func textFieldDidChange(_ sender : AnyObject) { - button.enabled = !textField.text.isEmpty - button.layer.borderColor = button.titleLabel?.textColor.CGColor + button.isEnabled = !(textField.text?.isEmpty)! + button.layer.borderColor = button.titleLabel?.textColor.cgColor } - @IBAction func buttonWasTapped(sender : AnyObject?) + @IBAction func buttonWasTapped(_ sender : AnyObject?) { textField.resignFirstResponder() spinner.startAnimating() @@ -68,24 +68,31 @@ class ViewController : UIViewController, UITextFieldDelegate { } else { TwitterClient.fetchAuthorizationToken(success: { () -> Void in self.fetchTweets() - }, failure: { (message) -> Void in + }, { [unowned self](message) -> Void in + DispatchQueue.main.async { self.showAlertViewWithMessage("Something went wrong getting token. \(message)") self.spinner.stopAnimating() + } }) } } func fetchTweets() { - TwitterClient.fetchTweetsForUser(textField.text.stringByRemovingWhitespace(), success: { (tweets) -> Void in - self.tweetsTableViewDelegate.tweets = tweets - self.tableView.reloadSections(NSIndexSet(index: 0), withRowAnimation: UITableViewRowAnimation.Fade) - self.tableView.scrollToTop(animated: true) - self.spinner.stopAnimating() - }, failure: { (message) -> Void in - self.showAlertViewWithMessage("Something went wrong getting tweets. \(message)") + guard let userFieldText = textField.text else { return } + TwitterClient.fetchTweetsForUser(userFieldText.stringByRemovingWhitespace(), success: { [unowned self](tweets) -> Void in + DispatchQueue.main.async { + self.tweetsTableViewDelegate.tweets = tweets + self.tableView.reloadSections(IndexSet([0]), with: UITableViewRowAnimation.fade) + self.tableView.scrollToTop(animated: true) self.spinner.stopAnimating() + } + }, failure: { [unowned self](message) -> Void in + DispatchQueue.main.async { + self.showAlertViewWithMessage("Something went wrong getting tweets. \(message)") + self.spinner.stopAnimating() + } }) } - + }