├── LICENSE ├── Podfile ├── Podfile.lock ├── R.generated.swift ├── README.md ├── TweeBox.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── 4faramita.xcuserdatad │ └── xcschemes │ ├── TweeBox.xcscheme │ └── xcschememanagement.plist ├── TweeBox.xcworkspace ├── contents.xcworkspacedata └── xcuserdata │ └── 4faramita.xcuserdatad │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── TweeBox ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon@2x.png │ │ └── icon@3x.png │ ├── Contents.json │ ├── delete.imageset │ │ ├── Contents.json │ │ ├── cross (1).png │ │ ├── cross (2).png │ │ └── cross.png │ ├── hashtag.imageset │ │ ├── Contents.json │ │ ├── hashtag (1).png │ │ ├── hashtag (2).png │ │ └── hashtag.png │ ├── launch_screen.imageset │ │ ├── Contents.json │ │ ├── launch_screen@2x.png │ │ └── launch_screen@3x.png │ ├── like_false.imageset │ │ ├── Contents.json │ │ ├── like (1).png │ │ ├── like (2).png │ │ └── like (3).png │ ├── like_true.imageset │ │ ├── Contents.json │ │ ├── like (4).png │ │ ├── like (5).png │ │ └── like (6).png │ ├── mention.imageset │ │ ├── Contents.json │ │ ├── at-2.png │ │ ├── at-3.png │ │ └── at.png │ ├── mention_thin.imageset │ │ ├── Contents.json │ │ ├── arroba (1).png │ │ ├── arroba (2).png │ │ └── arroba.png │ ├── picPlaceholder.imageset │ │ ├── Contents.json │ │ └── placeholder.png │ ├── picture.imageset │ │ ├── Contents.json │ │ ├── picture (1).png │ │ ├── picture (2).png │ │ └── picture (3).png │ ├── play_gif.imageset │ │ ├── Contents.json │ │ ├── gif-images-file-type-interface-symbol-of-stroke (1).png │ │ ├── gif-images-file-type-interface-symbol-of-stroke (2).png │ │ └── gif-images-file-type-interface-symbol-of-stroke.png │ ├── play_video.imageset │ │ ├── Contents.json │ │ ├── play-button (1).png │ │ ├── play-button (2).png │ │ └── play-button.png │ ├── profile.imageset │ │ ├── Contents.json │ │ ├── avatar (1).png │ │ ├── avatar (2).png │ │ └── avatar.png │ ├── profile_selected.imageset │ │ ├── Contents.json │ │ ├── man-user (1).png │ │ ├── man-user (2).png │ │ └── man-user.png │ ├── reply.imageset │ │ ├── Contents.json │ │ ├── reply (1).png │ │ ├── reply (2).png │ │ └── reply (3).png │ ├── reply_false.imageset │ │ ├── Contents.json │ │ ├── reply (4).png │ │ ├── reply (5).png │ │ └── reply (6).png │ ├── reply_true.imageset │ │ ├── Contents.json │ │ ├── reply (7).png │ │ ├── reply (8).png │ │ └── reply (9).png │ ├── retweet_false.imageset │ │ ├── Contents.json │ │ ├── retweet-arrows-couple-outline (1).png │ │ ├── retweet-arrows-couple-outline (2).png │ │ └── retweet-arrows-couple-outline.png │ ├── retweet_true.imageset │ │ ├── Contents.json │ │ ├── retweet-symbol (1).png │ │ ├── retweet-symbol (2).png │ │ └── retweet-symbol.png │ ├── twitter.imageset │ │ ├── Contents.json │ │ ├── twitter (6).png │ │ ├── twitter (7).png │ │ └── twitter (8).png │ └── twitter_selected.imageset │ │ ├── Contents.json │ │ ├── twitter (1).png │ │ ├── twitter (2).png │ │ └── twitter.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Constants.swift ├── Coordinates.swift ├── DataExtension.swift ├── EmbededPageViewController.swift ├── Entity.swift ├── Error.swift ├── FMBlurable.swift ├── FavoritePoster.swift ├── FavoriteTimelineParams.swift ├── FavoriteTimelineTableViewController.swift ├── FollowerTableViewController.swift ├── FollowingTableViewController.swift ├── FriendshipManager.swift ├── FriendshipPoster.swift ├── GeneralSearchViewController.swift ├── GeneralTweetTableViewCell.swift ├── HashTag.swift ├── HomeTimelineParams.swift ├── HomeTimelineTableViewController.swift ├── ImageContainerViewController.swift ├── ImageViewerViewController.swift ├── Info.plist ├── IntegerExtension.swift ├── InteractiveViewController.swift ├── LaunchScreenViewController.swift ├── LoginView.swift ├── LoginViewController.swift ├── MediaSize.swift ├── MediaUploadInitParams.swift ├── Mention.swift ├── MentionTimelineParams.swift ├── MentionTimelineTableViewController.swift ├── MultiUserParams.swift ├── NavigationViewControllerExtension.swift ├── OriginTweetView.swift ├── PageManagerViewController.swift ├── PannableViewController.swift ├── Params.swift ├── ParamsWithBounds.swift ├── ParamsWithCursor.swift ├── Place.swift ├── RESTfulClient.swift ├── RESTfulClientWithID.swift ├── RESTfulClientWithMedia.swift ├── ReplyTableViewController.swift ├── ReplyTimeline.swift ├── ResourceURL.swift ├── RetweetTableViewCell.swift ├── RetweeterID.swift ├── RetweetersIDParams.swift ├── SearchDialogView.swift ├── SearchResultType.swift ├── SearchTimeline.swift ├── SearchTimelineTableViewController.swift ├── SearchTweetParams.swift ├── SearchUsers.swift ├── SimplePostParams.swift ├── SimpleSearchViewController.swift ├── SimpleTweetComposer.swift ├── SimpleTweetPoster.swift ├── SimpleUserParams.swift ├── SingleTweet.swift ├── SingleTweetViewController.swift ├── SingleUser.swift ├── SpamUserParams.swift ├── StringExtension.swift ├── Timeline.swift ├── TimelineParams.swift ├── TimelineTableViewController.swift ├── TweeBox.xcdatamodeld │ ├── .xccurrentversion │ └── TweeBox.xcdatamodel │ │ └── contents ├── Tweet.swift ├── TweetClickableContentProtocol.swift ├── TweetComposer.swift ├── TweetComposerViewController.swift ├── TweetEntity.swift ├── TweetLabel.swift ├── TweetMedia.swift ├── TweetMediaContainerView.swift ├── TweetParams.swift ├── TweetPhoto.swift ├── TweetPhotoSize.swift ├── TweetPostParams.swift ├── TweetPoster.swift ├── TweetSymbol.swift ├── TweetTableViewCell.swift ├── TweetURL.swift ├── TweetVideoInfo.swift ├── TweetWithPicAndTextTableViewCell.swift ├── TweetWithPicAndTextUsingButtonTableViewCell.swift ├── TweetWithPicTableViewCell.swift ├── TweetWithTextTableViewCell.swift ├── TwitterAttributedContent.swift ├── TwitterDate.swift ├── TwitterUser.swift ├── TwitterUserTableViewCell.swift ├── UIViewExtension.swift ├── URLExtension.swift ├── UserList.swift ├── UserListParams.swift ├── UserListRetrieverProtocol.swift ├── UserListTableViewController.swift ├── UserParams.swift ├── UserProfileViewController.swift ├── UserTimelineParams.swift ├── UserTimelineTableViewController.swift ├── UsersSearchParams.swift └── VideoViewerViewController.swift ├── TweeBoxTests ├── Info.plist └── TweeBoxTests.swift └── TweeBoxUITests ├── Info.plist └── TweeBoxUITests.swift /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4faramita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '10.0' 3 | use_frameworks! 4 | # swift_version = "4.0" 5 | 6 | target 'TweeBox' do 7 | 8 | pod 'Reveal-SDK', :configurations => ['Debug'] 9 | 10 | pod 'SnapKit', '~> 4.0.0' 11 | pod 'TwitterKit' 12 | pod 'SwiftyJSON' 13 | pod 'Kingfisher', '~> 4.0' 14 | pod 'DateToolsSwift' 15 | pod 'AMScrollingNavbar' 16 | # pod 'Eureka' 17 | pod 'BMPlayer' 18 | pod "MXParallaxHeader" 19 | pod "VisualEffectView" 20 | # pod 'Kanna', '~> 2.1.0' 21 | pod 'PopupDialog', '~> 0.6' 22 | pod 'YYText' 23 | pod "ESPullToRefresh" 24 | pod 'SwipeCellKit' 25 | # pod 'STPopup' 26 | pod 'JDStatusBarNotification' 27 | pod 'R.swift' 28 | pod 'SwiftyAttributes' 29 | 30 | pod 'Gallery' 31 | pod 'Lightbox' 32 | 33 | 34 | # pod 'RealmSwift' 35 | end 36 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AMScrollingNavbar (4.0.4) 3 | - BMPlayer (1.0.0): 4 | - BMPlayer/Full (= 1.0.0) 5 | - BMPlayer/Core (1.0.0) 6 | - BMPlayer/Full (1.0.0): 7 | - BMPlayer/Core 8 | - NVActivityIndicatorView (~> 4.0.0) 9 | - SnapKit (~> 4.0.0) 10 | - Cache (4.0.4): 11 | - SwiftHash (~> 2.0.0) 12 | - DateToolsSwift (2.0.3) 13 | - ESPullToRefresh (2.7) 14 | - Gallery (2.0.2) 15 | - Hue (3.0.0) 16 | - Imaginary (3.0.0): 17 | - Cache (~> 4.0) 18 | - JDStatusBarNotification (1.5.6) 19 | - Kingfisher (4.2.0) 20 | - Lightbox (2.0.0): 21 | - Hue (~> 3.0) 22 | - Imaginary (~> 3.0) 23 | - MXParallaxHeader (0.6.1) 24 | - NVActivityIndicatorView (4.0.0): 25 | - NVActivityIndicatorView/Presenter (= 4.0.0) 26 | - NVActivityIndicatorView/Presenter (4.0.0) 27 | - PopupDialog (0.6.0) 28 | - R.swift (4.0.0): 29 | - R.swift.Library (~> 4.0.0) 30 | - R.swift.Library (4.0.0) 31 | - Reveal-SDK (11) 32 | - SnapKit (4.0.0) 33 | - SwiftHash (2.0.0) 34 | - SwiftyAttributes (4.1.0) 35 | - SwiftyJSON (3.1.4) 36 | - SwipeCellKit (2.0.0) 37 | - TwitterCore (3.0.2) 38 | - TwitterKit (3.2.1): 39 | - TwitterCore (>= 3.0.2) 40 | - VisualEffectView (3.0.0) 41 | - YYText (1.0.7) 42 | 43 | DEPENDENCIES: 44 | - AMScrollingNavbar 45 | - BMPlayer 46 | - DateToolsSwift 47 | - ESPullToRefresh 48 | - Gallery 49 | - JDStatusBarNotification 50 | - Kingfisher (~> 4.0) 51 | - Lightbox 52 | - MXParallaxHeader 53 | - PopupDialog (~> 0.6) 54 | - R.swift 55 | - Reveal-SDK 56 | - SnapKit (~> 4.0.0) 57 | - SwiftyAttributes 58 | - SwiftyJSON 59 | - SwipeCellKit 60 | - TwitterKit 61 | - VisualEffectView 62 | - YYText 63 | 64 | SPEC CHECKSUMS: 65 | AMScrollingNavbar: 3f35e65f5c97918c31be2179bda9ecb25dc5a3df 66 | BMPlayer: 06717ac5daf25d403329df24667abe2354dd9b17 67 | Cache: 1b3907ee2f5a457e7cfe9ac0be22dc9207f55a09 68 | DateToolsSwift: bf69bf9056d9e53c5c03f06e51f219b9b8b02f9e 69 | ESPullToRefresh: 119e2710b82b75536aab2bbe4883ba119fca35c8 70 | Gallery: a5cdf94495b789c622e8286dea6bf6f29a0b3429 71 | Hue: b8fe1e43eef13631331eebecb2198b68e2622f95 72 | Imaginary: 2765d293d425cbed3b07fa11642554cbaebe913d 73 | JDStatusBarNotification: d5829a8f2f291b23b8a54b87fac8bf2549945a2f 74 | Kingfisher: 9ee7e788d8ba07c3f21ce0d43f33cec310a4f781 75 | Lightbox: 83ec9f4aba4c79fc7bb9bcd263386df9969f09bb 76 | MXParallaxHeader: 858c403532ed933b381556a81ce333d7065ad182 77 | NVActivityIndicatorView: 54c9b93b6f25a4f467512680c0c90c09a1095cf2 78 | PopupDialog: 687af43af0f7b89948cd411ac701865f931bd6dc 79 | R.swift: d6a5ec2f55a8441dc0ed9f1f8b37d7d11ae85c66 80 | R.swift.Library: c3af34921024333546e23b70e70d0b4e0cffca75 81 | Reveal-SDK: 7fa13d04bb55db61ff2342a990cf731d5159361d 82 | SnapKit: a42d492c16e80209130a3379f73596c3454b7694 83 | SwiftHash: d2e09b13495447178cdfb8e46e54a5c46f15f5a9 84 | SwiftyAttributes: cef9463f6e7da1ab393896ca036c9ac37d8a406b 85 | SwiftyJSON: c2842d878f95482ffceec5709abc3d05680c0220 86 | SwipeCellKit: 349c61ffde96ac3ae51c2691a03e421b5b29ee98 87 | TwitterCore: e0420b01e02e7d20e94690e37f43e75de219ab8d 88 | TwitterKit: 205ac89824bbfe9e85a16c420d30bb7bcbedac45 89 | VisualEffectView: d47d295c68bf1b8648aa8e7d6eb4acf6dc18ea12 90 | YYText: 5c461d709e24d55a182d1441c41dc639a18a4849 91 | 92 | PODFILE CHECKSUM: d1c2d244ffe96253e4531923d1be380a59ae323b 93 | 94 | COCOAPODS: 1.3.1 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](https://user-images.githubusercontent.com/14917258/30247255-9fe2553e-9642-11e7-974a-d17ef132ef4f.png) 2 | 3 | ## TweeBox - A iOS Twitter Reader. 4 | 5 | > WARNING: TweeBox is still in a quite early phase of developing (after all, it's only one-month old now). So the bad news is, it still has many bugs and unfinished features; on the bright side, you will be there to witness its growth :) 6 | 7 | There are a lot of Twitter "clients", but this is not one of those -- TweeBox is a Twitter "reader", designed for those who would like to browse on twitter rather than speak loadly to the Twitter street. 8 | 9 | As a result, TweeBox is friendly to many tweet reading features, including but not limited to "favorite", "block" and "report spam". What's more, you can also easily delete your tweets by just a swipe. 10 | 11 | ![like](https://user-images.githubusercontent.com/14917258/29928805-65dfed2c-8e9c-11e7-838e-4ed73b6d70e9.png) 12 | 13 | ![delete](https://user-images.githubusercontent.com/14917258/29928804-65de5fde-8e9c-11e7-83cb-459827a3de08.png) 14 | 15 | Also, I'm currently living in a country where I have to use Twitter via proxy, which can be slow. So TweeBox also contains features like "do not show the image until it's fully loaded", so you can continue browsing your timeline while loading. 16 | 17 | ![loading](https://user-images.githubusercontent.com/14917258/29928806-65e3a7aa-8e9c-11e7-98aa-5eccf0fa7275.png) 18 | 19 | TweeBox also comes with a ton of other features and a decent UI design. More pictures: 20 | 21 | ![timeline](https://user-images.githubusercontent.com/14917258/29928816-6c180db4-8e9c-11e7-9147-de9fa9933f7a.png) 22 | 23 | 24 | ## Getting Started 25 | TweeBox is currently absent from Apple App Store (just for now). So if you want to try it,please do as follow: 26 | 27 | check out the source in a way you feel suits, for example: 28 | 29 | ``` 30 | git clone "https://github.com/4faramita/TweeBox.git" 31 | ``` 32 | 33 | Then 34 | ``` 35 | cd TweeBox 36 | pod install 37 | ``` 38 | Now you can open the `TweeBox.xcworkspace` file in Xcode. 39 | 40 | ## Change Log 41 | 2017-08-31: First release. Happy month-aversary, TweeBox! 42 | 43 | ## Roadmap 44 | ### Basic 45 | - [x] RESTful client 46 | - [x] Twitter API wrapper 47 | - [ ] Error handling 48 | 49 | ### Timeline: 50 | - [x] Basic timeline information display 51 | - [x] Timeline text parsing 52 | - [x] Timeline click action 53 | - [x] Click and wait indicator 54 | - [ ] Image viewer: 55 | - [x] Gestures, alerts 56 | - [x] Single Image viewer 57 | - [x] Share sheet 58 | - [ ] Multiple Image viewer 59 | - [x] GIF / video viewer 60 | 61 | - [x] Relative time 62 | - [ ] Content filter 63 | - [ ] User profile page: 64 | - [x] Basic infomation 65 | - [ ] Text parsing 66 | - [x] Click action (query) 67 | - [ ] Save to database 68 | - [x] Load upward and downward 69 | - [x] View replies 70 | - [x] View context 71 | 72 | #### Special: 73 | - [x] Tweet gesture 74 | - [ ] Waiting bubble 75 | 76 | ### Composer: 77 | - [x] Retweet 78 | - [x] Reply 79 | - [x] Compose new tweet 80 | 81 | ### Settings: 82 | 83 | ### Search: 84 | - [x] General search 85 | - [ ] Database search 86 | 87 | ### DM: 88 | 89 | 90 | -------------------------------------------------------------------------------- /TweeBox.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TweeBox.xcodeproj/xcuserdata/4faramita.xcuserdatad/xcschemes/TweeBox.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 76 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /TweeBox.xcodeproj/xcuserdata/4faramita.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TweeBox.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | B429859B1F29E8E600AFFB97 16 | 17 | primary 18 | 19 | 20 | B42985B21F29E8E700AFFB97 21 | 22 | primary 23 | 24 | 25 | B42985BD1F29E8E700AFFB97 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /TweeBox.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TweeBox.xcworkspace/xcuserdata/4faramita.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 14 | 15 | 23 | 24 | 32 | 33 | 41 | 42 | 43 | 44 | 45 | 47 | 53 | 54 | 62 | 63 | 64 | 65 | 66 | 68 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /TweeBox/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | import TwitterKit 12 | 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | var window: UIWindow? 18 | 19 | 20 | // Twitter Kit 21 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 22 | Twitter.sharedInstance().start(withConsumerKey:"KEY", consumerSecret:"SECRET") 23 | 24 | return true 25 | } 26 | 27 | func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { 28 | return Twitter.sharedInstance().application(app, open: url, options: options) 29 | } 30 | 31 | 32 | func applicationWillResignActive(_ application: UIApplication) { 33 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 34 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 35 | } 36 | 37 | func applicationDidEnterBackground(_ application: UIApplication) { 38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 40 | } 41 | 42 | func applicationWillEnterForeground(_ application: UIApplication) { 43 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | func applicationDidBecomeActive(_ application: UIApplication) { 47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 48 | } 49 | 50 | func applicationWillTerminate(_ application: UIApplication) { 51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 52 | // Saves changes in the application's managed object context before the application terminates. 53 | self.saveContext() 54 | } 55 | 56 | // MARK: - Core Data stack 57 | 58 | lazy var persistentContainer: NSPersistentContainer = { 59 | /* 60 | The persistent container for the application. This implementation 61 | creates and returns a container, having loaded the store for the 62 | application to it. This property is optional since there are legitimate 63 | error conditions that could cause the creation of the store to fail. 64 | */ 65 | let container = NSPersistentContainer(name: "TweeBox") 66 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 67 | if let error = error as NSError? { 68 | // Replace this implementation with code to handle the error appropriately. 69 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 70 | 71 | /* 72 | Typical reasons for an error here include: 73 | * The parent directory does not exist, cannot be created, or disallows writing. 74 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 75 | * The device is out of space. 76 | * The store could not be migrated to the current model version. 77 | Check the error message to determine what the actual problem was. 78 | */ 79 | fatalError("Unresolved error \(error), \(error.userInfo)") 80 | } 81 | }) 82 | return container 83 | }() 84 | 85 | // MARK: - Core Data Saving support 86 | 87 | func saveContext () { 88 | let context = persistentContainer.viewContext 89 | if context.hasChanges { 90 | do { 91 | try context.save() 92 | } catch { 93 | // Replace this implementation with code to handle the error appropriately. 94 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 95 | let nserror = error as NSError 96 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 97 | } 98 | } 99 | } 100 | 101 | // MARK: - Core Data Convenience 102 | 103 | static var persistentContainer: NSPersistentContainer { 104 | return (UIApplication.shared.delegate as! AppDelegate).persistentContainer 105 | } 106 | 107 | static var viewContext: NSManagedObjectContext { 108 | return persistentContainer.viewContext 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "icon@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon@3x.png", 43 | "scale" : "3x" 44 | } 45 | ], 46 | "info" : { 47 | "version" : 1, 48 | "author" : "xcode" 49 | } 50 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/AppIcon.appiconset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/AppIcon.appiconset/icon@2x.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/AppIcon.appiconset/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/AppIcon.appiconset/icon@3x.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/delete.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cross.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "cross (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "cross (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/delete.imageset/cross (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/delete.imageset/cross (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/delete.imageset/cross (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/delete.imageset/cross (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/delete.imageset/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/delete.imageset/cross.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/hashtag.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "hashtag.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "hashtag (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "hashtag (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/hashtag.imageset/hashtag (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/hashtag.imageset/hashtag (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/hashtag.imageset/hashtag (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/hashtag.imageset/hashtag (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/hashtag.imageset/hashtag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/hashtag.imageset/hashtag.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/launch_screen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "launch_screen@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "launch_screen@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/launch_screen.imageset/launch_screen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/launch_screen.imageset/launch_screen@2x.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/launch_screen.imageset/launch_screen@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/launch_screen.imageset/launch_screen@3x.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_false.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "like (1).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "like (2).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "like (3).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_false.imageset/like (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/like_false.imageset/like (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_false.imageset/like (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/like_false.imageset/like (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_false.imageset/like (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/like_false.imageset/like (3).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_true.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "like (4).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "like (5).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "like (6).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_true.imageset/like (4).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/like_true.imageset/like (4).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_true.imageset/like (5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/like_true.imageset/like (5).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/like_true.imageset/like (6).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/like_true.imageset/like (6).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "at.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "at-2.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "at-3.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention.imageset/at-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/mention.imageset/at-2.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention.imageset/at-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/mention.imageset/at-3.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention.imageset/at.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/mention.imageset/at.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention_thin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "arroba.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "arroba (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "arroba (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention_thin.imageset/arroba (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/mention_thin.imageset/arroba (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention_thin.imageset/arroba (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/mention_thin.imageset/arroba (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/mention_thin.imageset/arroba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/mention_thin.imageset/arroba.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/picPlaceholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "placeholder.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/picPlaceholder.imageset/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/picPlaceholder.imageset/placeholder.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/picture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "picture (1).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "picture (2).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "picture (3).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/picture.imageset/picture (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/picture.imageset/picture (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/picture.imageset/picture (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/picture.imageset/picture (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/picture.imageset/picture (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/picture.imageset/picture (3).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_gif.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "gif-images-file-type-interface-symbol-of-stroke.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "gif-images-file-type-interface-symbol-of-stroke (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "gif-images-file-type-interface-symbol-of-stroke (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_gif.imageset/gif-images-file-type-interface-symbol-of-stroke (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/play_gif.imageset/gif-images-file-type-interface-symbol-of-stroke (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_gif.imageset/gif-images-file-type-interface-symbol-of-stroke (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/play_gif.imageset/gif-images-file-type-interface-symbol-of-stroke (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_gif.imageset/gif-images-file-type-interface-symbol-of-stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/play_gif.imageset/gif-images-file-type-interface-symbol-of-stroke.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "play-button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "play-button (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "play-button (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_video.imageset/play-button (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/play_video.imageset/play-button (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_video.imageset/play-button (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/play_video.imageset/play-button (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/play_video.imageset/play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/play_video.imageset/play-button.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "avatar (2).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "avatar (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "avatar.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile.imageset/avatar (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/profile.imageset/avatar (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile.imageset/avatar (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/profile.imageset/avatar (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile.imageset/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/profile.imageset/avatar.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "man-user (2).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "man-user (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "man-user.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile_selected.imageset/man-user (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/profile_selected.imageset/man-user (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile_selected.imageset/man-user (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/profile_selected.imageset/man-user (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/profile_selected.imageset/man-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/profile_selected.imageset/man-user.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "reply (1).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "reply (2).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "reply (3).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply.imageset/reply (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply.imageset/reply (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply.imageset/reply (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply.imageset/reply (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply.imageset/reply (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply.imageset/reply (3).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_false.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "reply (4).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "reply (5).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "reply (6).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_false.imageset/reply (4).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply_false.imageset/reply (4).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_false.imageset/reply (5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply_false.imageset/reply (5).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_false.imageset/reply (6).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply_false.imageset/reply (6).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_true.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "reply (7).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "reply (8).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "reply (9).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_true.imageset/reply (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply_true.imageset/reply (7).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_true.imageset/reply (8).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply_true.imageset/reply (8).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/reply_true.imageset/reply (9).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/reply_true.imageset/reply (9).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_false.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "retweet-arrows-couple-outline.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "retweet-arrows-couple-outline (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "retweet-arrows-couple-outline (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_false.imageset/retweet-arrows-couple-outline (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/retweet_false.imageset/retweet-arrows-couple-outline (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_false.imageset/retweet-arrows-couple-outline (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/retweet_false.imageset/retweet-arrows-couple-outline (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_false.imageset/retweet-arrows-couple-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/retweet_false.imageset/retweet-arrows-couple-outline.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_true.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "retweet-symbol.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "retweet-symbol (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "retweet-symbol (2).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_true.imageset/retweet-symbol (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/retweet_true.imageset/retweet-symbol (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_true.imageset/retweet-symbol (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/retweet_true.imageset/retweet-symbol (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/retweet_true.imageset/retweet-symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/retweet_true.imageset/retweet-symbol.png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "twitter (8).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "twitter (7).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "twitter (6).png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter.imageset/twitter (6).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/twitter.imageset/twitter (6).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter.imageset/twitter (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/twitter.imageset/twitter (7).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter.imageset/twitter (8).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/twitter.imageset/twitter (8).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "twitter (2).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "twitter (1).png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "twitter.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter_selected.imageset/twitter (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/twitter_selected.imageset/twitter (1).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter_selected.imageset/twitter (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/twitter_selected.imageset/twitter (2).png -------------------------------------------------------------------------------- /TweeBox/Assets.xcassets/twitter_selected.imageset/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4faramita/TweeBox/ac5539e5eb7f5fad1dfa3737f08cadf04c729189/TweeBox/Assets.xcassets/twitter_selected.imageset/twitter.png -------------------------------------------------------------------------------- /TweeBox/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /TweeBox/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import TwitterKit 12 | 13 | struct Constants { 14 | 15 | static var selfID: String { 16 | return Twitter.sharedInstance().sessionStore.session()?.userID ?? "-1" 17 | } 18 | 19 | static let tweetLimitPerRefresh = "100" 20 | static let userLimitPerRefresh = "100" 21 | 22 | static let picQuality = MediaSize.large 23 | 24 | static let aspectRatioWidth: CGFloat = 16 25 | static let aspectRatioHeight: CGFloat = 9 26 | 27 | static let normalAspectRatio = aspectRatioHeight / aspectRatioWidth 28 | static let thinAspectRatio = normalAspectRatio * 2 29 | 30 | static let picCornerRadius: CGFloat = 3 31 | 32 | static let defaultProfileRadius = ProfileRadius.round.rawValue 33 | 34 | static let picFadeInDuration = 0.2 35 | 36 | static let naturalReading = true 37 | 38 | static let profileImageRadius: CGFloat = 50 39 | static let profilePanelDragOffset: CGFloat = 100 40 | static let profileToolbarHeight: CGFloat = 50 41 | static let contentUnifiedOffset: CGFloat = 20 42 | 43 | static let themeColor = UIColor.orange // (red: 0, green: 0, blue: 1, alpha: 1) 44 | static let lightenThemeColor = UIColor.gray // (red: 0, green: 0, blue: 1, alpha: 0.5) 45 | } 46 | 47 | enum ProfileRadius: CGFloat { 48 | case round = 200 49 | case square = 20 50 | } 51 | -------------------------------------------------------------------------------- /TweeBox/Coordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinates.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/5. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | // The inner coordinates array is formatted as geoJSON (longitude first, then latitude) 10 | 11 | import Foundation 12 | import SwiftyJSON 13 | 14 | struct Coordinates { 15 | 16 | public var type: String 17 | 18 | public var coordinates: [Any] 19 | 20 | init(with json: JSON) { 21 | 22 | type = json["type"].string ?? "Point" 23 | 24 | switch type { 25 | case "Point": 26 | coordinates = json["coordinates"].arrayValue.map({ $0.floatValue }) 27 | case "Polygon": 28 | coordinates = [json["coordinates"][0].arrayValue.map({ [$0[0].floatValue, $0[1].floatValue] })] 29 | default: 30 | coordinates = [0.0, 0.0] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TweeBox/DataExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataExtension.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Data { 12 | var MIMEType: String { 13 | var values = [UInt8](repeating:0, count:1) 14 | self.copyBytes(to: &values, count: 1) 15 | 16 | let ext: String 17 | switch (values[0]) { 18 | case 0xFF: 19 | ext = "image/jpeg" 20 | case 0x89: 21 | ext = "image/png" 22 | case 0x47: 23 | ext = "image/gif" 24 | case 0x49, 0x4D : 25 | ext = "image/tiff" 26 | default: 27 | ext = "image/png" 28 | } 29 | return ext 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TweeBox/EmbededPageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbededPageViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/13. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EmbededPageViewController: UIPageViewController { 12 | 13 | public var currentIndex: Int! 14 | 15 | public var imageViewers: [ImageViewerViewController]! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | dataSource = self 21 | } 22 | 23 | func viewImageViewerViewController(_ index: Int) -> ImageViewerViewController? { 24 | guard storyboard != nil 25 | else { 26 | return nil 27 | } 28 | 29 | return imageViewers[index] 30 | } 31 | } 32 | 33 | 34 | extension EmbededPageViewController: UIPageViewControllerDataSource { 35 | 36 | func pageViewController(_ pageViewController: UIPageViewController, 37 | viewControllerBefore viewController: UIViewController) -> UIViewController? { 38 | 39 | if let viewController = viewController as? ImageViewerViewController, 40 | let index = viewController.photoIndex, 41 | index > 0 { 42 | return viewImageViewerViewController(index - 1) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func pageViewController(_ pageViewController: UIPageViewController, 49 | viewControllerAfter viewController: UIViewController) -> UIViewController? { 50 | 51 | if let viewController = viewController as? ImageViewerViewController, 52 | let index = viewController.photoIndex, 53 | (index + 1) < imageViewers.count { 54 | return viewImageViewerViewController(index + 1) 55 | } 56 | 57 | return nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /TweeBox/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/5. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | struct Entity { 13 | 14 | typealias TweetSymbol = Hashtag 15 | 16 | public var hashtags: [Hashtag] 17 | public var urls: [TweetURL] 18 | public var userMentions: [Mention] 19 | public var symbols: [TweetSymbol] 20 | 21 | public var media: [TweetMedia]? 22 | public var realMedia: [TweetMedia]? 23 | public var mediaToShare: [TweetMedia]? 24 | public var thumbMedia: [TweetMedia]? 25 | 26 | 27 | init(with json: JSON, and extendedJson: JSON) { 28 | 29 | hashtags = json["hashtags"].arrayValue.map { Hashtag(with: $0) } 30 | urls = json["urls"].arrayValue.map { TweetURL(with: $0) } 31 | userMentions = json["user_mentions"].arrayValue.map { Mention(with: $0) } 32 | symbols = json["symbols"].arrayValue.map { TweetSymbol(with: $0) } 33 | 34 | if extendedJson.null == nil { 35 | // there exists extended_json 36 | media = extendedJson["media"].arrayValue.map { TweetMedia(with: $0, quality: MediaSize.small) } 37 | realMedia = extendedJson["media"].arrayValue.map { TweetMedia(with: $0, quality: Constants.picQuality) } 38 | mediaToShare = extendedJson["media"].arrayValue.map { TweetMedia(with: $0, quality: .nonNormal) } 39 | thumbMedia = extendedJson["media"].arrayValue.map { TweetMedia(with: $0, quality: MediaSize.thumb) } 40 | // media in extended_entities 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TweeBox/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Error: Int { 12 | case netWorkError 13 | case unauthorized 14 | } 15 | -------------------------------------------------------------------------------- /TweeBox/FMBlurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FMBlurable.swift 3 | // FMBlurable 4 | // 5 | // Created by SIMON_NON_ADMIN on 18/09/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | // Thanks to romainmenke (https://twitter.com/romainmenke) for hint on a larger sample... 9 | 10 | import UIKit 11 | 12 | 13 | protocol Blurable 14 | { 15 | var layer: CALayer { get } 16 | var subviews: [UIView] { get } 17 | var frame: CGRect { get } 18 | var superview: UIView? { get } 19 | 20 | func addSubview(_: UIView) 21 | func removeFromSuperview() 22 | 23 | func blur(blurRadius: CGFloat) 24 | func unBlur() 25 | 26 | var isBlurred: Bool { get } 27 | } 28 | 29 | extension Blurable 30 | { 31 | func blur(blurRadius: CGFloat) 32 | { 33 | if self.superview == nil 34 | { 35 | return 36 | } 37 | 38 | UIGraphicsBeginImageContextWithOptions(CGSize(width: frame.width, height: frame.height), false, 1) 39 | 40 | layer.render(in: UIGraphicsGetCurrentContext()!) 41 | 42 | let image = UIGraphicsGetImageFromCurrentImageContext() 43 | 44 | UIGraphicsEndImageContext(); 45 | 46 | guard let blur = CIFilter(name: "CIGaussianBlur"), 47 | let this = self as? UIView else 48 | { 49 | return 50 | } 51 | 52 | blur.setValue(CIImage(image: image!), forKey: kCIInputImageKey) 53 | // UNWRAP 54 | blur.setValue(blurRadius, forKey: kCIInputRadiusKey) 55 | 56 | let ciContext = CIContext(options: nil) 57 | 58 | let result = blur.value(forKey: kCIOutputImageKey) as! CIImage! 59 | 60 | let boundingRect = CGRect(x:0, 61 | y: 0, 62 | width: frame.width, 63 | height: frame.height) 64 | 65 | let cgImage = ciContext.createCGImage(result!, from: boundingRect) 66 | // UNWRAP 67 | 68 | let filteredImage = UIImage(cgImage: cgImage!) 69 | // UNWRAP 70 | 71 | let blurOverlay = BlurOverlay() 72 | blurOverlay.frame = boundingRect 73 | 74 | blurOverlay.image = filteredImage 75 | blurOverlay.contentMode = UIViewContentMode.left 76 | 77 | if let superview = superview as? UIStackView, 78 | let index = (superview as UIStackView).arrangedSubviews.index(of: this) 79 | { 80 | removeFromSuperview() 81 | superview.insertArrangedSubview(blurOverlay, at: index) 82 | } 83 | else 84 | { 85 | blurOverlay.frame.origin = frame.origin 86 | 87 | UIView.transition(from: this, 88 | to: blurOverlay, 89 | duration: 0.2, 90 | options: UIViewAnimationOptions.curveEaseIn, 91 | completion: nil) 92 | } 93 | 94 | objc_setAssociatedObject(this, 95 | &BlurableKey.blurable, 96 | blurOverlay, 97 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 98 | } 99 | 100 | func unBlur() 101 | { 102 | guard let this = self as? UIView, 103 | let blurOverlay = objc_getAssociatedObject(self as? UIView, &BlurableKey.blurable) as? BlurOverlay else 104 | { 105 | return 106 | } 107 | 108 | if let superview = blurOverlay.superview as? UIStackView, 109 | let index = (blurOverlay.superview as! UIStackView).arrangedSubviews.index(of: blurOverlay) 110 | { 111 | blurOverlay.removeFromSuperview() 112 | superview.insertArrangedSubview(this, at: index) 113 | } 114 | else 115 | { 116 | this.frame.origin = blurOverlay.frame.origin 117 | 118 | UIView.transition(from: blurOverlay, 119 | to: this, 120 | duration: 0.2, 121 | options: UIViewAnimationOptions.curveEaseIn, 122 | completion: nil) 123 | } 124 | 125 | objc_setAssociatedObject(this, 126 | &BlurableKey.blurable, 127 | nil, 128 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 129 | } 130 | 131 | var isBlurred: Bool 132 | { 133 | return objc_getAssociatedObject(self as? UIView, &BlurableKey.blurable) is BlurOverlay 134 | } 135 | } 136 | 137 | extension UIView: Blurable 138 | { 139 | } 140 | 141 | class BlurOverlay: UIImageView 142 | { 143 | } 144 | 145 | struct BlurableKey 146 | { 147 | static var blurable = "blurable" 148 | } 149 | -------------------------------------------------------------------------------- /TweeBox/FavoritePoster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritePoster.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class FavoritePoster: SimpleTweetPoster { 13 | 14 | override func postData(_ handler: @escaping (Tweet?) -> Void) { 15 | 16 | if Constants.selfID != "-1" { 17 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 18 | 19 | print(">>> FavoritePoster >> \(params.getParams())") 20 | 21 | client.getData() { data in 22 | if let data = data { 23 | let json = JSON(data: data) 24 | if json.null == nil { 25 | let tweet = Tweet(with: json) 26 | handler(tweet) 27 | } else { 28 | handler(nil) 29 | } 30 | } else { 31 | handler(nil) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TweeBox/FavoriteTimelineParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteTimelineParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class FavoriteTimelineParams: UserTimelineParams { 12 | 13 | override init(of userID: String, sinceID: String? = nil, maxID: String? = nil, excludeReplies: Bool = false, includeRetweets: Bool = true) { 14 | 15 | super.init(of: userID, sinceID: sinceID, maxID: maxID, excludeReplies: excludeReplies, includeRetweets: includeRetweets) 16 | resourceURL = ResourceURL.favorites_list 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /TweeBox/FavoriteTimelineTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteTimelineTableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FavoriteTimelineTableViewController: TimelineTableViewController { 12 | 13 | var userID: String { 14 | return Constants.selfID 15 | } 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | self.navigationItem.leftBarButtonItem = nil 21 | } 22 | 23 | override func refreshTimeline(handler: (() -> Void)?) { 24 | 25 | let params = FavoriteTimelineParams(of: userID) 26 | 27 | let timeline = Timeline( 28 | maxID: maxID, 29 | sinceID: sinceID, 30 | fetchNewer: fetchNewer, 31 | resourceURL: params.resourceURL, 32 | params: params 33 | ) 34 | 35 | timeline.fetchData { [weak self] (maxID, sinceID, tweets) in 36 | 37 | if (self?.maxID == nil) && (self?.sinceID == nil) { 38 | if let sinceID = sinceID { 39 | self?.sinceID = sinceID 40 | } 41 | if let maxID = maxID { 42 | self?.maxID = maxID 43 | } 44 | } else { 45 | if (self?.fetchNewer)! { 46 | if let sinceID = sinceID { 47 | self?.sinceID = sinceID 48 | } 49 | } else { 50 | if let maxID = maxID { 51 | self?.maxID = maxID 52 | } 53 | } 54 | 55 | } 56 | 57 | if tweets.count > 0 { 58 | 59 | self?.insertNewTweets(with: tweets) 60 | 61 | let cells = self?.tableView.visibleCells 62 | if cells != nil { 63 | for cell in cells! { 64 | let indexPath = self?.tableView.indexPath(for: cell) 65 | if let tweetCell = cell as? GeneralTweetTableViewCell { 66 | tweetCell.section = indexPath?.section 67 | } 68 | } 69 | 70 | } 71 | 72 | } 73 | 74 | if let handler = handler { 75 | handler() 76 | } 77 | } 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /TweeBox/FollowerTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowerTableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/15. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FollowerTableViewController: UserListTableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | resourceURL = ResourceURL.followers_list 17 | } 18 | 19 | init(followerListParams: UserListParams) { 20 | self.userListParams = followerListParams 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TweeBox/FollowingTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowingTableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/15. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FollowingTableViewController: UserListTableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | resourceURL = ResourceURL.followings_list 17 | } 18 | 19 | init(followingListParams: UserListParams) { 20 | self.userListParams = followingListParams 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TweeBox/FriendshipManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendshipManager.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct FriendshipManager { 12 | 13 | var userID: String 14 | 15 | func follow(handler: @escaping (Bool, TwitterUser?) -> Void) { 16 | 17 | let params = SimpleUserParams(userID: userID, screenName: nil) 18 | 19 | let dataRetriever = FriendshipPoster(userParams: params, resourceURL: ResourceURL.friendships_create) 20 | 21 | dataRetriever.postData { (user) in 22 | if self.userID == user?.id { 23 | handler(true, user) 24 | } else { 25 | handler(false, user) 26 | } 27 | } 28 | } 29 | 30 | 31 | func unfollow(handler: @escaping (Bool, TwitterUser?) -> Void) { 32 | 33 | let params = SimpleUserParams(userID: userID, screenName: nil) 34 | 35 | let dataRetriever = FriendshipPoster(userParams: params, resourceURL: ResourceURL.friendships_destroy) 36 | 37 | dataRetriever.postData { (user) in 38 | if self.userID == user?.id { 39 | handler(true, user) 40 | } else { 41 | handler(false, user) 42 | } 43 | } 44 | } 45 | 46 | func block(handler: @escaping (Bool, TwitterUser?) -> Void) { 47 | 48 | let params = SimpleUserParams(userID: userID, screenName: nil) 49 | 50 | let dataRetriever = FriendshipPoster(userParams: params, resourceURL: ResourceURL.blocks_create) 51 | 52 | dataRetriever.postData { (user) in 53 | if self.userID == user?.id { 54 | handler(true, user) 55 | } else { 56 | handler(false, user) 57 | } 58 | } 59 | } 60 | 61 | 62 | func unblock(handler: @escaping (Bool, TwitterUser?) -> Void) { 63 | 64 | let params = SimpleUserParams(userID: userID, screenName: nil) 65 | 66 | let dataRetriever = FriendshipPoster(userParams: params, resourceURL: ResourceURL.blocks_destroy) 67 | 68 | dataRetriever.postData { (user) in 69 | if self.userID == user?.id { 70 | handler(true, user) 71 | } else { 72 | handler(false, user) 73 | } 74 | } 75 | } 76 | 77 | 78 | func reportSpam(block: Bool?, handler: @escaping (Bool, TwitterUser?) -> Void) { 79 | 80 | let params = SpamUserParams(userID: userID, screenName: nil, performBlock: block) 81 | 82 | let dataRetriever = FriendshipPoster(userParams: params, resourceURL: ResourceURL.users_report_spam) 83 | 84 | dataRetriever.postData { (user) in 85 | if self.userID == user?.id { 86 | handler(true, user) 87 | } else { 88 | handler(false, user) 89 | } 90 | } 91 | } 92 | 93 | 94 | } 95 | -------------------------------------------------------------------------------- /TweeBox/FriendshipPoster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendshipPoster.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class FriendshipPoster { 13 | 14 | public var resourceURL: (String, String) 15 | public var userParams: SimpleUserParams 16 | 17 | init(userParams: SimpleUserParams, resourceURL: (String, String)) { 18 | self.resourceURL = resourceURL 19 | self.userParams = userParams 20 | } 21 | 22 | 23 | func postData(_ handler: @escaping (TwitterUser?) -> Void) { 24 | 25 | if Constants.selfID != "-1" { 26 | 27 | let client = RESTfulClient(resource: resourceURL, params: userParams.getParams()) 28 | 29 | print(">>> FavoritePoster >> \(userParams.getParams())") 30 | 31 | client.getData() { data in 32 | if let data = data { 33 | let json = JSON(data: data) 34 | if json.null == nil { 35 | let user = TwitterUser(with: json) 36 | handler(user) 37 | } else { 38 | handler(nil) 39 | } 40 | } else { 41 | handler(nil) 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /TweeBox/GeneralSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralSearchViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SnapKit 11 | import PopupDialog 12 | 13 | class GeneralSearchViewController: UIViewController { 14 | 15 | @IBOutlet weak var dialogView: SearchDialogView! 16 | 17 | fileprivate var keyword: String? { 18 | return dialogView.keyword 19 | } 20 | 21 | fileprivate var fetchedUser: TwitterUser? 22 | 23 | 24 | @IBAction func findUser(_ sender: UIButton) { 25 | 26 | fetchUser() { [weak self] (user) in 27 | 28 | if let user = user { 29 | self?.fetchedUser = user 30 | self?.performSegue(withIdentifier: "Show User", sender: self) 31 | } else { 32 | self?.alertForNoSuchUser() 33 | } 34 | } 35 | 36 | } 37 | 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | setTextField() 43 | dialogView.initSearchButtons() 44 | } 45 | 46 | private func setTextField() { 47 | 48 | dialogView.inputTextField.delegate = self 49 | 50 | dialogView.snp.makeConstraints { (make) in 51 | make.height.equalTo(350) 52 | } 53 | 54 | dialogView.inputTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) 55 | } 56 | 57 | @objc private func textFieldDidChange(_ textField: UITextField) { 58 | 59 | Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] (timer) in 60 | self?.dialogView.keyword = textField.text 61 | } 62 | } 63 | } 64 | 65 | // Segue related 66 | extension GeneralSearchViewController { 67 | 68 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 69 | if let keyword = keyword, let identifier = segue.identifier { 70 | switch identifier { 71 | case "Show Tweets": 72 | if let searchTimelineViewController = segue.destination.content as? SearchTimelineTableViewController { 73 | searchTimelineViewController.query = keyword 74 | searchTimelineViewController.navigationItem.title = "\"\(keyword)\"" 75 | } 76 | 77 | case "Show Tweets with Hashtag": 78 | if let searchTimelineViewController = segue.destination.content as? SearchTimelineTableViewController { 79 | // if let keyword = keyword { 80 | searchTimelineViewController.query = "%23\(keyword)" 81 | searchTimelineViewController.navigationItem.title = "#\(keyword)" 82 | // } 83 | } 84 | 85 | case "Show User": 86 | if let profileViewController = segue.destination.content as? UserTimelineTableViewController { 87 | if let user = fetchedUser { 88 | profileViewController.user = user 89 | } 90 | } 91 | 92 | case "Show Users": 93 | if let userListTableViewController = segue.destination.content as? UserListTableViewController { 94 | 95 | let simpleUserListRetriever = SearchUsers( 96 | params: UsersSearchParams(query: keyword), 97 | resourceURL: ResourceURL.users_search 98 | ) 99 | 100 | userListTableViewController.userListRetriever = simpleUserListRetriever 101 | userListTableViewController.navigationItem.title = "\"\(keyword)\"" 102 | } 103 | 104 | default: 105 | return 106 | } 107 | } 108 | } 109 | 110 | 111 | 112 | fileprivate func alertForNoSuchUser() { 113 | 114 | print(">>> no user") 115 | 116 | let popup = PopupDialog(title: "Cannot Find User @\(keyword ?? "")", message: "Please check your input.", image: nil) 117 | let cancelButton = CancelButton(title: "OK") { } 118 | popup.addButton(cancelButton) 119 | present(popup, animated: true, completion: nil) 120 | 121 | } 122 | 123 | 124 | fileprivate func fetchUser(_ handler: @escaping (TwitterUser?) -> Void) { 125 | 126 | SingleUser( 127 | params: UserParams(userID: nil, screenName: keyword), 128 | resourceURL: ResourceURL.user_show 129 | ).fetchData { (singleUser) in 130 | handler(singleUser) 131 | } 132 | } 133 | } 134 | 135 | 136 | extension GeneralSearchViewController: UITextFieldDelegate { 137 | 138 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 139 | textField.resignFirstResponder() 140 | return true 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /TweeBox/HashTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HashTag.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class Hashtag: TweetEntity { 13 | 14 | var text: String 15 | 16 | override init(with json: JSON) { 17 | 18 | text = json["text"].stringValue 19 | 20 | super.init(with: json) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TweeBox/HomeTimelineParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeTimelineParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/9. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class HomeTimelineParams: TimelineParams { 12 | 13 | override init(sinceID: String? = nil, maxID: String? = nil, excludeReplies: Bool? = false, includeRetweets: Bool? = true) { 14 | 15 | super.init(excludeReplies: nil, includeRetweets: nil) 16 | resourceURL = ResourceURL.statuses_home_timeline 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TweeBox/HomeTimelineTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeTimelineTableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/14. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HomeTimelineTableViewController: UserTimelineTableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | userID = Constants.selfID 17 | // let's user database here 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TweeBox/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | It requires access to camera to upload media directly from camera. 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | CFBundleURLTypes 40 | 41 | 42 | CFBundleURLSchemes 43 | 44 | twitterkit-yuo33GhvlWXA465CE1wrIhvss 45 | 46 | 47 | 48 | NSPhotoLibraryUsageDescription 49 | Need the access to your photo library to save to or upload from it. 50 | LSApplicationQueriesSchemes 51 | 52 | twitter 53 | twitterauth 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /TweeBox/IntegerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntegerExtension.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/21. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Int { 12 | 13 | var shortExpression: String { 14 | 15 | let shortenExpression: String 16 | 17 | if self > 10000 { 18 | shortenExpression = "\(Int(Float(self) / 1000))k" 19 | } else { 20 | shortenExpression = "\(self)" 21 | } 22 | 23 | return shortenExpression 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TweeBox/LaunchScreenViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchScreenViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LaunchScreenViewController: UIViewController { 12 | 13 | @IBOutlet weak var imageView: UIImageView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | imageView.image = UIImage(named: "launch_screen") 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /TweeBox/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoginView: UIView { 12 | 13 | /* 14 | // Only override draw() if you perform custom drawing. 15 | // An empty implementation adversely affects performance during animation. 16 | override func draw(_ rect: CGRect) { 17 | // Drawing code 18 | } 19 | */ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /TweeBox/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SafariServices 11 | import TwitterKit 12 | 13 | class LoginViewController: UIViewController, SFSafariViewControllerDelegate { 14 | 15 | override func viewDidAppear(_ animated: Bool) { 16 | super.viewWillAppear(animated) 17 | addLoginButton() 18 | 19 | if Twitter.sharedInstance().sessionStore.session()?.userID != nil { 20 | performSegue(withIdentifier: "login", sender: self) 21 | } 22 | } 23 | 24 | private func addLoginButton() { 25 | let logInButton = TWTRLogInButton(logInCompletion: { [weak self] session, error in 26 | if session != nil { 27 | print("signed in as \(session!.userName)") 28 | self?.performSegue(withIdentifier: "login", sender: nil) 29 | } else if error != nil { 30 | print("error: \(error!.localizedDescription)") 31 | } 32 | }) 33 | 34 | logInButton.center = self.view.center 35 | self.view.addSubview(logInButton) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TweeBox/MediaSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaSize.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/8. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum MediaSize: String { 12 | case thumb = ":thumb" 13 | case small = ":small" 14 | case medium = ":medium" 15 | case large = ":large" 16 | 17 | case nonNormal = "" 18 | case bigger = "_bigger" 19 | case max = "_400x400" 20 | } 21 | -------------------------------------------------------------------------------- /TweeBox/MediaUploadInitParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaUploadInitParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class MediaUploadInitParams: ParamsProtocol { 13 | 14 | private let command = "INIT" 15 | 16 | var size: String // total_bytes 17 | 18 | var type: String // media_type 19 | 20 | var category: String // media_category 21 | 22 | // additional_owners 23 | 24 | 25 | init(size: String, type: String, category: String) { 26 | 27 | self.size = size 28 | self.type = type 29 | self.category = category 30 | } 31 | 32 | 33 | func getParams() -> [String : Any] { 34 | 35 | var params = [String: String]() 36 | 37 | params["command"] = command 38 | 39 | params["total_bytes"] = size 40 | 41 | params["media_type"] = type 42 | 43 | params["media_category"] = category 44 | 45 | return params 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /TweeBox/Mention.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mention.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | 13 | class Mention: TweetEntity { 14 | var id: String 15 | var screenName: String 16 | var name: String 17 | 18 | override init(with json: JSON) { 19 | 20 | id = json["id_str"].stringValue 21 | screenName = json["screen_name"].stringValue 22 | name = json["name"].stringValue 23 | 24 | super.init(with: json) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TweeBox/MentionTimelineParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MentionTimelineParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/23. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MentionTimelineParams: TimelineParams { 12 | 13 | init(sinceID: String? = nil, maxID: String? = nil) { 14 | 15 | super.init(sinceID: sinceID, maxID: maxID, excludeReplies: nil, includeRetweets: nil) 16 | 17 | resourceURL = ResourceURL.statuses_mentions_timeline 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /TweeBox/MentionTimelineTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MentionTimelineTableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/23. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MentionTimelineTableViewController: TimelineTableViewController { 12 | 13 | override func refreshTimeline(handler: (() -> Void)?) { 14 | 15 | let mentionTimelineParams = MentionTimelineParams() 16 | 17 | let timeline = Timeline( 18 | maxID: maxID, 19 | sinceID: sinceID, 20 | fetchNewer: fetchNewer, 21 | resourceURL: mentionTimelineParams.resourceURL, 22 | params: mentionTimelineParams 23 | ) 24 | 25 | timeline.fetchData { [weak self] (maxID, sinceID, tweets) in 26 | 27 | if (self?.maxID == nil) && (self?.sinceID == nil) { 28 | if let sinceID = sinceID { 29 | self?.sinceID = sinceID 30 | } 31 | if let maxID = maxID { 32 | self?.maxID = maxID 33 | } 34 | } else { 35 | if (self?.fetchNewer)! { 36 | if let sinceID = sinceID { 37 | self?.sinceID = sinceID 38 | } 39 | } else { 40 | if let maxID = maxID { 41 | self?.maxID = maxID 42 | } 43 | } 44 | 45 | } 46 | 47 | if tweets.count > 0 { 48 | 49 | self?.insertNewTweets(with: tweets) 50 | 51 | let cells = self?.tableView.visibleCells 52 | if cells != nil { 53 | for cell in cells! { 54 | let indexPath = self?.tableView.indexPath(for: cell) 55 | if let tweetCell = cell as? GeneralTweetTableViewCell { 56 | tweetCell.section = indexPath?.section 57 | } 58 | } 59 | 60 | } 61 | 62 | } 63 | 64 | if let handler = handler { 65 | handler() 66 | } 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /TweeBox/MultiUserParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiUserParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/23. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MultiUserParams: ParamsProtocol { 12 | 13 | public var userID: [String]? 14 | 15 | public var screenName: [String]? 16 | 17 | public var includeEntities = false 18 | 19 | init(userID: [String]?, screenName: [String]?) { 20 | self.userID = userID 21 | self.screenName = screenName 22 | } 23 | 24 | 25 | public func getParams() -> [String: Any] { 26 | 27 | var params = [String: Any]() 28 | 29 | guard (userID != nil || screenName != nil) else { 30 | return params 31 | } 32 | 33 | if userID != nil { 34 | params["user_id"] = userID 35 | } else { 36 | params["screen_name"] = screenName 37 | } 38 | 39 | params["include_entities"] = includeEntities.description 40 | 41 | return params 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /TweeBox/NavigationViewControllerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationViewControllerExtension.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/12. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | var content: UIViewController { 13 | if let navigationViewController = self as? UINavigationController { 14 | return navigationViewController.visibleViewController ?? self 15 | } else { 16 | return self 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TweeBox/OriginTweetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OriginTweetView.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/19. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | class OriginTweetView: UIView { 13 | 14 | public var originTweet: Tweet! { 15 | didSet { 16 | setOriginTweet() 17 | } 18 | } 19 | 20 | private var thumbImages: [TweetMedia]? { 21 | return originTweet?.entities?.thumbMedia 22 | } 23 | 24 | private var hasMedia: Bool { 25 | return (thumbImages != nil && thumbImages!.count > 0) 26 | } 27 | 28 | private func setOriginTweet() { 29 | 30 | for subview in subviews { 31 | subview.removeFromSuperview() 32 | } 33 | 34 | if hasMedia { 35 | print(">>> has media >> \(originTweet.text)") 36 | self.snp.makeConstraints({ (make) in 37 | make.height.equalTo(150) 38 | }) 39 | } 40 | 41 | 42 | var thumbImageView: UIImageView? 43 | 44 | let upperBorder = UIButton() 45 | addSubview(upperBorder) 46 | upperBorder.backgroundColor = .lightGray 47 | upperBorder.alpha = 0.5 48 | upperBorder.isUserInteractionEnabled = false 49 | upperBorder.snp.makeConstraints { (make) in 50 | make.top.equalTo(self) 51 | make.width.equalTo(self).multipliedBy(0.8) 52 | make.height.equalTo(1) 53 | make.centerX.equalTo(self) 54 | } 55 | 56 | let lowerBorder = UIButton() 57 | addSubview(lowerBorder) 58 | lowerBorder.backgroundColor = .lightGray 59 | lowerBorder.alpha = 0.5 60 | lowerBorder.isUserInteractionEnabled = false 61 | lowerBorder.snp.makeConstraints { (make) in 62 | make.bottom.equalTo(self) 63 | make.width.equalTo(self).multipliedBy(0.8) 64 | make.height.equalTo(1) 65 | make.centerX.equalTo(self) 66 | } 67 | 68 | if hasMedia { 69 | 70 | thumbImageView = UIImageView() 71 | addSubview(thumbImageView!) 72 | thumbImageView?.snp.makeConstraints { (make) in 73 | make.trailing.equalTo(self) 74 | make.top.equalTo(self) 75 | make.height.equalTo(150) 76 | make.width.equalTo(150) 77 | } 78 | thumbImageView!.kf.setImage(with: thumbImages?[0].mediaURL) 79 | } 80 | 81 | let profileImageView = UIImageView() 82 | addSubview(profileImageView) 83 | profileImageView.snp.makeConstraints { (make) in 84 | make.top.equalTo(self).offset(8) 85 | make.leading.equalTo(upperBorder).offset(-16) 86 | make.height.equalTo(32) 87 | make.width.equalTo(32) 88 | } 89 | profileImageView.cutToRound(radius: 16) 90 | profileImageView.kf.setImage(with: originTweet?.user.profileImageURL) 91 | 92 | let originNameLabel = UILabel() 93 | addSubview(originNameLabel) 94 | originNameLabel.snp.makeConstraints { (make) in 95 | make.top.equalTo(profileImageView) 96 | make.leading.equalTo(profileImageView.snp.trailing).offset(5) 97 | if hasMedia { 98 | make.trailing.greaterThanOrEqualTo(thumbImageView!.snp.leading).offset(-5) 99 | } else { 100 | make.trailing.greaterThanOrEqualTo(self) 101 | } 102 | } 103 | originNameLabel.text = originTweet.user.name 104 | originNameLabel.font = UIFont.preferredFont(forTextStyle: .caption1) 105 | originNameLabel.lineBreakMode = .byTruncatingTail 106 | 107 | let originScreenNameLabel = UILabel() 108 | addSubview(originScreenNameLabel) 109 | originScreenNameLabel.snp.makeConstraints { (make) in 110 | make.bottom.equalTo(profileImageView) 111 | make.leading.equalTo(profileImageView.snp.trailing).offset(5) 112 | if hasMedia { 113 | make.trailing.greaterThanOrEqualTo(thumbImageView!.snp.leading).offset(-5) 114 | } else { 115 | make.trailing.greaterThanOrEqualTo(self) 116 | } 117 | } 118 | originScreenNameLabel.text = "@\(originTweet.user.screenName)" 119 | originScreenNameLabel.textColor = .darkGray 120 | originScreenNameLabel.font = UIFont.preferredFont(forTextStyle: .caption2) 121 | originScreenNameLabel.lineBreakMode = .byTruncatingTail 122 | 123 | let originTextLabel = UILabel() 124 | addSubview(originTextLabel) 125 | originTextLabel.text = originTweet.text 126 | originTextLabel.font = UIFont.preferredFont(forTextStyle: .caption1) 127 | originTextLabel.lineBreakMode = .byTruncatingTail 128 | originTextLabel.numberOfLines = 0 129 | originTextLabel.snp.makeConstraints { (make) in 130 | make.top.equalTo(profileImageView.snp.bottom).offset(3) 131 | make.leading.equalTo(upperBorder) 132 | make.bottom.equalTo(self).offset(-5) 133 | if hasMedia { 134 | make.trailing.equalTo(thumbImageView!.snp.leading).offset(-5) 135 | } else { 136 | make.trailing.equalTo(upperBorder) 137 | } 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /TweeBox/PageManagerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageManagerViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/14. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PageManagerViewController: PannableViewController { 12 | 13 | var pageViewController: EmbededPageViewController! 14 | 15 | public var currentIndex: Int! 16 | 17 | public var imageViewers: [ImageViewerViewController]! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | pageViewController = self.childViewControllers.first as! EmbededPageViewController 23 | pageViewController.currentIndex = currentIndex 24 | pageViewController.imageViewers = imageViewers 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /TweeBox/PannableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PannableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/11. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class PannableViewController: UIViewController { 13 | 14 | var panGestureRecognizer: UIPanGestureRecognizer? 15 | var originalPosition: CGPoint? 16 | var currentPositionTouched: CGPoint? 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:))) 22 | view.addGestureRecognizer(panGestureRecognizer!) 23 | } 24 | 25 | @objc func panGestureAction(_ panGesture: UIPanGestureRecognizer) { 26 | 27 | let translation = panGesture.translation(in: view) 28 | 29 | if panGesture.state == .began { 30 | originalPosition = view.center 31 | currentPositionTouched = panGesture.location(in: view) 32 | } else if panGesture.state == .changed { 33 | view.frame.origin = CGPoint( 34 | x: translation.x, 35 | y: translation.y 36 | ) 37 | } else if panGesture.state == .ended { 38 | let velocity = panGesture.velocity(in: view) 39 | 40 | if velocity.y >= 500 { 41 | UIView.animate(withDuration: 0.2 42 | , animations: { 43 | self.view.frame.origin = CGPoint( 44 | x: self.view.frame.origin.x, 45 | y: self.view.frame.size.height 46 | ) 47 | }, completion: { (isCompleted) in 48 | if isCompleted { 49 | self.dismiss(animated: true, completion: nil) 50 | } 51 | }) 52 | } else if velocity.y <= -500 { 53 | UIView.animate(withDuration: 0.2 54 | , animations: { 55 | self.view.frame.origin = CGPoint( 56 | x: self.view.frame.origin.x, 57 | y: -self.view.frame.size.height 58 | ) 59 | }, completion: { (isCompleted) in 60 | if isCompleted { 61 | self.dismiss(animated: true, completion: nil) 62 | } 63 | }) 64 | } else { 65 | UIView.animate(withDuration: 0.2, animations: { 66 | self.view.center = self.originalPosition! 67 | }) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /TweeBox/Params.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Params.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ParamsProtocol { 12 | func getParams() -> [String: Any] 13 | } 14 | -------------------------------------------------------------------------------- /TweeBox/ParamsWithBounds.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParamsWithBounds.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ParamsWithBoundsProtocol: ParamsProtocol { 12 | 13 | var sinceID: String? { get set } 14 | var maxID: String? { get set } 15 | } 16 | -------------------------------------------------------------------------------- /TweeBox/ParamsWithCursor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParamsWithCursor.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ParamsWithCursorProtocol: ParamsProtocol { 12 | 13 | var cursor: String? { get set } 14 | } 15 | -------------------------------------------------------------------------------- /TweeBox/Place.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Place.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/5. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | struct Place { 13 | 14 | public var attributes: PlaceAttributes 15 | public var boundingBox: Coordinates 16 | public var country: String 17 | public var countryCode: String 18 | public var fullName: String 19 | public var id: String 20 | public var name: String 21 | public var placeType: String 22 | public var url: URL? 23 | 24 | init(with json: JSON) { 25 | 26 | attributes = PlaceAttributes(with: json["attributes"]) 27 | boundingBox = Coordinates(with: json["bounding_box"]) 28 | country = json["country"].stringValue 29 | countryCode = json["country_code"].stringValue 30 | fullName = json["full_name"].stringValue 31 | id = json["id"].stringValue 32 | name = json["name"].stringValue 33 | placeType = json["place_type"].stringValue 34 | url = URL(string: json["url"].stringValue) 35 | } 36 | 37 | 38 | struct PlaceAttributes { 39 | 40 | public var streetAddress: String? 41 | public var locality: String? 42 | // the city the place is in 43 | public var region: String? 44 | // the administrative region the place is in 45 | public var iso3: String? 46 | // the country code 47 | public var postalCode: String? 48 | // in the preferred local format for the place 49 | public var phone: String? 50 | // in the preferred local format for the place, include long distance code 51 | public var twitter: String? 52 | // twitter screen-name, without @ 53 | public var url: URL? 54 | // official/canonical URL for place 55 | // public var appID: String? 56 | // An ID or comma separated list of IDs representing the place in the applications place database. 57 | 58 | init(with json: JSON) { 59 | 60 | streetAddress = json["street_address"].string 61 | locality = json["locality"].string 62 | region = json["region"].string 63 | iso3 = json["iso3"].string 64 | postalCode = json["postal_code"].string 65 | phone = json["phone"].string 66 | twitter = json["twitter"].string 67 | url = URL(string: json["url"].stringValue) 68 | // appID = json["app:id"].string 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /TweeBox/RESTfulClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RESTfulClient.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | class RESTfulClient { 13 | 14 | var resource: (url: String, method: String) 15 | 16 | var params: [String: Any]? 17 | 18 | // private var dataJSON: Data? 19 | 20 | init(resource: (url: String, method: String), params: [String: Any]?) { 21 | self.resource = resource 22 | self.params = params 23 | } 24 | 25 | public func getData(_ handler: @escaping (_ data: Data?) -> Void) { 26 | 27 | 28 | if let userID = Twitter.sharedInstance().sessionStore.session()?.userID { 29 | 30 | let client = TWTRAPIClient(userID: userID) 31 | var clientError : NSError? 32 | 33 | 34 | 35 | let request = client.urlRequest(withMethod: resource.method, url: resource.url, parameters: params, error: &clientError) 36 | 37 | client.sendTwitterRequest(request) { (response, data, connectionError) -> Void in 38 | if connectionError != nil { 39 | print("Error: \(connectionError!)") 40 | } 41 | 42 | if data != nil { 43 | handler(data!) 44 | } else { 45 | print(">>> NO DATA") 46 | handler(nil) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TweeBox/RESTfulClientWithID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RESTfulClientWithID.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | class RESTfulClientWithID: RESTfulClient { 13 | 14 | override func getData(_ handler: @escaping (Data?) -> Void) { 15 | 16 | if let userID = Twitter.sharedInstance().sessionStore.session()?.userID { 17 | 18 | let client = TWTRAPIClient(userID: userID) 19 | var clientError : NSError? 20 | 21 | if let tweetID = params?["id"] as? String { 22 | 23 | let url = resource.url.replacingOccurrences(of: ":id", with: tweetID) 24 | print(">>> url \(url)") 25 | 26 | let request = client.urlRequest(withMethod: resource.method, url: url, parameters: nil, error: &clientError) 27 | 28 | client.sendTwitterRequest(request) { (response, data, connectionError) -> Void in 29 | if connectionError != nil { 30 | print("Error: \(connectionError!)") 31 | } 32 | 33 | if data != nil { 34 | handler(data!) 35 | } else { 36 | print(">>> NO DATA") 37 | handler(nil) 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TweeBox/RESTfulClientWithMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RESTfulClientWithMedia.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | class RESTfulClientWithMedia { 13 | 14 | var imageData: Data 15 | 16 | private var type: String { 17 | return imageData.MIMEType 18 | } 19 | 20 | 21 | init(imageData: Data) { 22 | self.imageData = imageData 23 | } 24 | 25 | 26 | func getData(_ handler: @escaping (String?) -> Void) { 27 | 28 | if let userID = Twitter.sharedInstance().sessionStore.session()?.userID { 29 | 30 | let client = TWTRAPIClient(userID: userID) 31 | 32 | print(">>> type >> \(type)") 33 | 34 | client.uploadMedia(imageData, contentType: type, completion: { (mediaID, error) in 35 | 36 | guard error == nil else { 37 | print(">>> client error >> \(error.debugDescription)") 38 | return 39 | } 40 | 41 | if let mediaID = mediaID { 42 | handler(mediaID) 43 | } else { 44 | handler(nil) 45 | } 46 | }) 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /TweeBox/ReplyTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyTableViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/23. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ReplyTableViewController: SearchTimelineTableViewController { 12 | 13 | @IBOutlet weak var tweetInfoContainerView: UIView! 14 | 15 | private var retweet: Tweet? 16 | 17 | public var tweet: Tweet! { 18 | didSet { 19 | if let originTweet = tweet.retweetedStatus, tweet.text.hasPrefix("RT @") { 20 | retweet = tweet 21 | tweet = originTweet 22 | } 23 | 24 | if tweetID == nil { 25 | tweetID = tweet?.id 26 | } 27 | } 28 | } 29 | 30 | public var tweetID: String! { 31 | didSet { 32 | if tweet == nil { 33 | setTweet() 34 | } 35 | } 36 | } 37 | 38 | override var resultType: SearchResultType { 39 | return .recent 40 | } 41 | 42 | // MARK - Header height 43 | public var cellTextLabelHeight: CGFloat? 44 | private var hasMedia: Bool { 45 | if let media = tweet?.entities?.realMedia, media.count > 0 { 46 | return true 47 | } else { 48 | return false 49 | } 50 | } 51 | 52 | override func viewDidLayoutSubviews() { 53 | super.viewDidLayoutSubviews() 54 | 55 | for subview in (tweetInfoContainerView.subviews[0].subviews) { 56 | if subview.frame.height > 0, subview.subviews.count != 0 { 57 | tweetInfoContainerView?.frame.size.height = subview.frame.height + CGFloat(30) 58 | } 59 | } 60 | } 61 | 62 | 63 | override func viewWillAppear(_ animated: Bool) { 64 | super.viewWillAppear(animated) 65 | 66 | refreshTimeline(handler: nil) 67 | } 68 | 69 | 70 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 71 | 72 | if segue.identifier == "Single Tweet Info" { 73 | if let singleTweetViewController = segue.destination.content as? SingleTweetViewController { 74 | singleTweetViewController.tweet = tweet 75 | } 76 | } 77 | 78 | super.prepare(for: segue, sender: sender) 79 | } 80 | 81 | override func refreshTimeline(handler: (() -> Void)?) { 82 | 83 | let screenName = "%40\(tweet?.user.screenName ?? "")" 84 | 85 | let replyTimelineParams = SearchTweetParams( 86 | query: screenName, 87 | resultType: self.resultType, 88 | until: nil, 89 | sinceID: nil, 90 | maxID: nil, 91 | includeEntities: nil, 92 | resourceURL: ResourceURL.search_tweets 93 | ) 94 | 95 | let replyTimeline = ReplyTimeline( 96 | maxID: maxID, 97 | sinceID: sinceID, 98 | fetchNewer: fetchNewer, 99 | resourceURL: replyTimelineParams.resourceURL, 100 | timelineParams: replyTimelineParams, 101 | mainTweetID: tweetID ?? "" 102 | ) 103 | 104 | replyTimeline.fetchData { [weak self] (maxID, sinceID, tweets) in 105 | 106 | if (self?.maxID == nil) && (self?.sinceID == nil) { 107 | if let sinceID = sinceID { 108 | self?.sinceID = sinceID 109 | } 110 | if let maxID = maxID { 111 | self?.maxID = maxID 112 | } 113 | } else { 114 | if (self?.fetchNewer)! { 115 | if let sinceID = sinceID { 116 | self?.sinceID = sinceID 117 | } 118 | } else { 119 | if let maxID = maxID { 120 | self?.maxID = maxID 121 | } 122 | } 123 | 124 | } 125 | 126 | if tweets.count > 0 { 127 | 128 | self?.insertNewTweets(with: tweets) 129 | 130 | let cells = self?.tableView.visibleCells 131 | if cells != nil { 132 | for cell in cells! { 133 | let indexPath = self?.tableView.indexPath(for: cell) 134 | if let tweetCell = cell as? GeneralTweetTableViewCell { 135 | tweetCell.section = indexPath?.section 136 | } 137 | } 138 | 139 | } 140 | 141 | } 142 | 143 | if let handler = handler { 144 | handler() 145 | }} 146 | 147 | } 148 | 149 | private func setTweet() { 150 | SingleTweet( 151 | params: TweetParams(of: tweetID!), 152 | resourceURL: ResourceURL.statuses_show_id 153 | ).fetchData { [weak self] (tweet) in 154 | if tweet != nil { 155 | self?.tweet = tweet 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /TweeBox/ReplyTimeline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyTimeline.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class ReplyTimeline: SearchTimeline { 13 | 14 | public var mainTweetID: String 15 | 16 | init(maxID: String?, sinceID: String?, fetchNewer: Bool = true, resourceURL: (String, String), timelineParams: ParamsWithBoundsProtocol, mainTweetID: String) { 17 | 18 | self.mainTweetID = mainTweetID 19 | 20 | super.init(maxID: maxID, sinceID: sinceID, resourceURL: resourceURL, params: timelineParams) 21 | } 22 | 23 | 24 | override func addToTimeline(_ tweet: Tweet) { 25 | 26 | if let repliedID = tweet.inReplyToStatusID, repliedID == mainTweetID { 27 | self.timeline.append(tweet) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /TweeBox/ResourceURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceURL.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ResourceURL { 12 | 13 | // GET 14 | 15 | static let user_lookup = (url: "https://api.twitter.com/1.1/users/lookup.json", method: "GET") 16 | /* 17 | Returns fully-hydrated user objects for up to 100 users per request, as specified by comma-separated values passed to the user_id and/or screen_name parameters. 18 | 19 | This method is especially useful when used in conjunction with collections of user IDs returned from GET friends / ids and GET followers / ids. 20 | */ 21 | 22 | static let user_show = (url: "https://api.twitter.com/1.1/users/show.json", method: "GET") 23 | 24 | static let users_search = (url: "https://api.twitter.com/1.1/users/search.json", method: "GET") 25 | 26 | 27 | static let statuses_show_id = (url: "https://api.twitter.com/1.1/statuses/show.json", method: "GET") 28 | 29 | static let statuses_retweeters_ids = (url: "https://api.twitter.com/1.1/statuses/retweeters/ids.json", method: "GET") 30 | 31 | static let statuses_home_timeline = (url: "https://api.twitter.com/1.1/statuses/home_timeline.json", method: "GET") 32 | 33 | static let statuses_user_timeline = (url: "https://api.twitter.com/1.1/statuses/user_timeline.json", method: "GET") 34 | 35 | static let statuses_mentions_timeline = (url: "https://api.twitter.com/1.1/statuses/mentions_timeline.json", method: "GET") 36 | 37 | static let favorites_list = (url: "https://api.twitter.com/1.1/favorites/list.json", method: "GET") 38 | 39 | 40 | static let followers_list = (url: "https://api.twitter.com/1.1/followers/list.json", method: "GET") 41 | 42 | static let followings_list = (url: "https://api.twitter.com/1.1/friends/list.json", method: "GET") 43 | 44 | 45 | static let search_tweets = (url: "https://api.twitter.com/1.1/search/tweets.json", method: "GET") 46 | 47 | 48 | 49 | // POST 50 | 51 | static let statuses_update = (url: "https://api.twitter.com/1.1/statuses/update.json", method: "POST") 52 | 53 | static let statuses_destroy_id = (url: "https://api.twitter.com/1.1/statuses/destroy/:id.json", method: "POST") 54 | 55 | static let statuses_unretweet_id = (url: "https://api.twitter.com/1.1/statuses/unretweet/:id.json", method: "POST") 56 | 57 | static let statuses_retweet_id = (url: "https://api.twitter.com/1.1/statuses/retweet/:id.json", method: "POST") 58 | 59 | 60 | static let favorites_create = (url: "https://api.twitter.com/1.1/favorites/create.json", method: "POST") 61 | 62 | static let favorites_destroy = (url: "https://api.twitter.com/1.1/favorites/destroy.json", method: "POST") 63 | 64 | 65 | static let friendships_create = (url: "https://api.twitter.com/1.1/friendships/create.json", method: "POST") 66 | 67 | static let friendships_destroy = (url: "https://api.twitter.com/1.1/friendships/destroy.json", method: "POST") 68 | 69 | static let friendships_update = (url: "https://api.twitter.com/1.1/friendships/update.json", method: "POST") 70 | 71 | 72 | static let blocks_create = (url: "https://api.twitter.com/1.1/blocks/create.json", method: "POST") 73 | 74 | static let blocks_destroy = (url: "https://api.twitter.com/1.1/blocks/destroy.json", method: "POST") 75 | 76 | static let users_report_spam = (url: "https://api.twitter.com/1.1/users/report_spam.json", method: "POST") 77 | 78 | 79 | static let media_upload = (url: "https://upload.twitter.com/1.1/media/upload.json", method: "POST") 80 | } 81 | -------------------------------------------------------------------------------- /TweeBox/RetweetTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetweetTableViewCell.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/18. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | import DateToolsSwift 12 | import SnapKit 13 | 14 | 15 | class RetweetTableViewCell: TweetWithTextTableViewCell { 16 | 17 | 18 | @IBOutlet weak var originTweetView: OriginTweetView! 19 | 20 | @IBAction func originTweetTapped(byReactingTo: UIGestureRecognizer) { 21 | guard let section = section, let row = row else { return } 22 | delegate?.originTweetTapped(section: section, row: row) 23 | } 24 | 25 | override func updateUI() { 26 | 27 | super.updateUI() 28 | 29 | addTapGesture() 30 | 31 | originTweetView.originTweet = tweet?.quotedStatus 32 | 33 | } 34 | 35 | private func addTapGesture() { 36 | let tapOnOriginTweet = UITapGestureRecognizer( 37 | target: self, 38 | action: #selector(originTweetTapped(byReactingTo:)) 39 | ) 40 | tapOnOriginTweet.numberOfTapsRequired = 1 41 | tapOnOriginTweet.numberOfTouchesRequired = 1 42 | originTweetView.addGestureRecognizer(tapOnOriginTweet) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /TweeBox/RetweeterID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetweeterID.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/22. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | 13 | class RetweeterID { 14 | 15 | // private var userIDList: [String]? 16 | 17 | public var nextCursor = "-1" 18 | public var previousCursor = "-1" 19 | // cursor == "0" indicates the corresponding direction is at the end 20 | 21 | public var fetchOlder = true 22 | 23 | public var params: RetweetersIDParams 24 | public var resourceURL: (String, String) 25 | 26 | init(resourceURL: (String, String), params: RetweetersIDParams, fetchOlder: Bool?, nextCursor: String?, previousCursor: String?) { 27 | 28 | self.resourceURL = resourceURL 29 | 30 | self.params = params 31 | 32 | if let fetchOlder = fetchOlder { 33 | self.fetchOlder = fetchOlder 34 | } 35 | 36 | if let nextCursor = nextCursor { 37 | self.nextCursor = nextCursor 38 | } 39 | 40 | if let previousCursor = previousCursor { 41 | self.previousCursor = previousCursor 42 | } 43 | } 44 | 45 | 46 | public func fetchData(_ handler: @escaping (String, String, [String]) -> Void) { 47 | 48 | if Constants.selfID != "-1" { 49 | 50 | if fetchOlder, previousCursor != "-1" { 51 | params.cursor = previousCursor 52 | } 53 | 54 | if !fetchOlder, nextCursor != "-1" { 55 | params.cursor = nextCursor 56 | } 57 | 58 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 59 | print(resourceURL) 60 | print(params.getParams()) 61 | client.getData() { data in 62 | if let data = data { 63 | let json = JSON(data: data) 64 | print(">>> rt json >> \(json)") 65 | 66 | self.nextCursor = json["next_cursor_str"].stringValue 67 | self.previousCursor = json["previous_cursor_str"].stringValue 68 | 69 | let idList = json["ids"].arrayValue.map({ $0.stringValue }) 70 | 71 | handler(self.nextCursor, self.previousCursor, idList) 72 | } 73 | } 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /TweeBox/RetweetersIDParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetweetersIDParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/22. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RetweetersIDParams: ParamsWithCursorProtocol { 12 | 13 | public var id: String 14 | public var cursor: String? = "-1" 15 | 16 | // stringify_ids 17 | 18 | init(id: String, cursor: String?) { 19 | self.id = id 20 | if let cursor = cursor { 21 | self.cursor = cursor 22 | } 23 | } 24 | 25 | public func getParams() -> [String: Any] { 26 | 27 | var params = [String: String]() 28 | 29 | params["id"] = id 30 | 31 | if let cursor = cursor, cursor != "-1" { 32 | params["cursor"] = cursor 33 | } 34 | 35 | 36 | params["stringify_ids"] = "true" 37 | 38 | return params 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /TweeBox/SearchDialogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchDialogView.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/25. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SnapKit 11 | 12 | class SearchDialogView: UIView { 13 | 14 | public var keyword: String? { 15 | didSet { 16 | setSearchButtons() 17 | } 18 | } 19 | 20 | @IBOutlet weak var inputTextField: UITextField! 21 | @IBOutlet weak var searchTweetButton: UIButton! 22 | @IBOutlet weak var searchHashtagButton: UIButton! 23 | @IBOutlet weak var findUserButton: UIButton! 24 | @IBOutlet weak var searchUserButton: UIButton! 25 | 26 | private var buttons: [UIButton]! 27 | 28 | 29 | func initSearchButtons() { 30 | 31 | buttons = [searchTweetButton, searchHashtagButton, findUserButton, searchUserButton] 32 | 33 | searchTweetButton.setTitle("Search Tweet", for: .disabled) 34 | // searchTweetButton.isEnabled = false 35 | 36 | searchHashtagButton.setTitle("Search Hashtag", for: .disabled) 37 | // searchHashtagButton.isEnabled = false 38 | 39 | findUserButton.setTitle("Find User", for: .disabled) 40 | // findUserButton.isEnabled = false 41 | 42 | searchUserButton.setTitle("Search User", for: .disabled) 43 | // searchUserButton.isEnabled = false 44 | 45 | for button in buttons { 46 | 47 | button.isEnabled = false 48 | 49 | button.cutToRound(radius: 5) 50 | 51 | button.layer.borderWidth = 1.0 52 | button.layer.borderColor = UIColor.lightGray.cgColor 53 | 54 | // button.backgroundColor = .gray 55 | 56 | button.setTitleColor(.darkGray, for: .normal) 57 | button.setTitleColor(.lightGray, for: .disabled) 58 | 59 | } 60 | 61 | } 62 | 63 | 64 | private func setSearchButtons() { 65 | if let keyword = keyword, !(keyword.isEmpty) { 66 | 67 | searchTweetButton.setTitle("Tweet With \"\(keyword)\"", for: .normal) 68 | if !(searchTweetButton.isEnabled) { 69 | searchTweetButton.isEnabled = true 70 | } 71 | 72 | if keyword.isLegit { 73 | searchHashtagButton.setTitle("Hashtag \"#\(keyword)\"", for: .normal) 74 | if !(searchHashtagButton.isEnabled) { 75 | searchHashtagButton.isEnabled = true 76 | } 77 | 78 | findUserButton.setTitle("User \"@\(keyword)\"", for: .normal) 79 | if !(findUserButton.isEnabled) { 80 | findUserButton.isEnabled = true 81 | } 82 | 83 | searchUserButton.setTitle("User With \"\(keyword)\"", for: .normal) 84 | if !(searchUserButton.isEnabled) { 85 | searchUserButton.isEnabled = true 86 | } 87 | } else { 88 | 89 | for index in 1...3 { 90 | let button = buttons[index] 91 | if button.isEnabled { 92 | button.isEnabled = false 93 | } 94 | } 95 | } 96 | } else { 97 | 98 | for button in buttons { 99 | if button.isEnabled { 100 | button.isEnabled = false 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /TweeBox/SearchResultType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultType.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SearchResultType: String { 12 | case mixed = "mixed" 13 | case recent = "recent" 14 | case popular = "popular" 15 | } 16 | -------------------------------------------------------------------------------- /TweeBox/SearchTimeline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchTimeline.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/26. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class SearchTimeline: Timeline { 13 | 14 | override func appendTweet(from json: JSON) { 15 | 16 | for (_, tweetJSON) in json["statuses"] { 17 | if tweetJSON.null == nil { 18 | let tweet = Tweet(with: tweetJSON) 19 | addToTimeline(tweet) 20 | } 21 | } 22 | } 23 | 24 | func addToTimeline(_ tweet: Tweet) { 25 | self.timeline.append(tweet) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TweeBox/SearchTimelineTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 4faramita on 2017/8/24. 3 | // Copyright (c) 2017 4faramita. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | 9 | 10 | class SearchTimelineTableViewController: TimelineTableViewController { 11 | 12 | var query: String? { 13 | didSet { 14 | refreshTimeline(handler: nil) 15 | } 16 | } 17 | 18 | var resultType: SearchResultType { 19 | return .recent 20 | } 21 | 22 | @IBAction func done(_ sender: UIBarButtonItem) { 23 | dismiss(animated: true, completion: nil) 24 | } 25 | 26 | override func setAndPerformSegueForHashtag() { 27 | 28 | let destinationViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "SearchTimelineViewController") 29 | 30 | let segue = UIStoryboardSegue(identifier: "Show Tweets with Hashtag", source: self, destination: destinationViewController) { 31 | self.navigationController?.show(destinationViewController, sender: self) 32 | } 33 | 34 | self.prepare(for: segue, sender: self) 35 | segue.perform() 36 | 37 | } 38 | 39 | 40 | override func refreshTimeline(handler: (() -> Void)?) { 41 | 42 | let replyTimelineParams = SearchTweetParams( 43 | query: self.query ?? "", 44 | resultType: self.resultType, 45 | until: nil, 46 | sinceID: nil, // this two will be managed 47 | maxID: nil, // in timeline data retriever 48 | includeEntities: true, 49 | resourceURL: ResourceURL.search_tweets 50 | ) 51 | 52 | let searchTimeline = SearchTimeline( 53 | maxID: maxID, 54 | sinceID: sinceID, 55 | fetchNewer: fetchNewer, 56 | resourceURL: replyTimelineParams.resourceURL, 57 | params: replyTimelineParams 58 | ) 59 | 60 | searchTimeline.fetchData { [weak self] (maxID, sinceID, tweets) in 61 | 62 | if (self?.maxID == nil) && (self?.sinceID == nil) { 63 | if let sinceID = sinceID { 64 | self?.sinceID = sinceID 65 | } 66 | if let maxID = maxID { 67 | self?.maxID = maxID 68 | } 69 | } else { 70 | if (self?.fetchNewer)! { 71 | if let sinceID = sinceID { 72 | self?.sinceID = sinceID 73 | } 74 | } else { 75 | if let maxID = maxID { 76 | self?.maxID = maxID 77 | } 78 | } 79 | 80 | } 81 | 82 | print(">>> tweets >> \(tweets.count)") 83 | 84 | if tweets.count > 0 { 85 | 86 | self?.insertNewTweets(with: tweets) 87 | 88 | let cells = self?.tableView.visibleCells 89 | if cells != nil { 90 | for cell in cells! { 91 | let indexPath = self?.tableView.indexPath(for: cell) 92 | if let tweetCell = cell as? GeneralTweetTableViewCell { 93 | tweetCell.section = indexPath?.section 94 | } 95 | } 96 | } 97 | } 98 | 99 | if let handler = handler { 100 | handler() 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /TweeBox/SearchTweetParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchTweetParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/24. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SearchTweetParams: ParamsWithBoundsProtocol { 12 | 13 | public var query: String // qd 14 | // A UTF-8, URL-encoded search query of 500 characters maximum, including operators. 15 | 16 | // public var geocode 17 | // public var lang 18 | // public var locale 19 | 20 | public var resultType: SearchResultType 21 | 22 | public var count = Constants.tweetLimitPerRefresh 23 | 24 | public var until: Date? 25 | /* 26 | Returns tweets created before the given date. 27 | Date should be formatted as YYYY-MM-DD. 28 | Keep in mind that the search index has a 7-day limit. 29 | In other words, no tweets will be found for a date older than one week. 30 | */ 31 | 32 | public var sinceID: String? 33 | public var maxID: String? 34 | 35 | public var includeEntities = true 36 | 37 | public var resourceURL: (String, String)! 38 | 39 | 40 | init( 41 | query: String, 42 | resultType: SearchResultType, 43 | until: Date?, 44 | sinceID: String?, 45 | maxID: String?, 46 | includeEntities: Bool?, 47 | resourceURL: (String, String) 48 | ) { 49 | self.query = query 50 | self.resultType = resultType 51 | 52 | if let until = until { 53 | self.until = until 54 | } 55 | if let sinceID = sinceID { 56 | self.sinceID = sinceID 57 | } 58 | if let maxID = maxID { 59 | self.maxID = maxID 60 | } 61 | if let includeEntities = includeEntities { 62 | self.includeEntities = includeEntities 63 | } 64 | 65 | self.resourceURL = resourceURL 66 | } 67 | 68 | public func getParams() -> [String: Any] { 69 | 70 | var params = [String: String]() 71 | 72 | params["q"] = query 73 | 74 | params["result_type"] = resultType.rawValue 75 | 76 | params["count"] = count 77 | 78 | // if let until = until { 79 | // do things about until 80 | // } 81 | 82 | if let sinceID = sinceID { 83 | params["since_id"] = sinceID 84 | } 85 | 86 | if let maxID = maxID { 87 | params["max_id"] = maxID 88 | } 89 | 90 | params["include_entities"] = includeEntities.description 91 | 92 | return params 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /TweeBox/SearchUsers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchUsers.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/26. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class SearchUsers: UserListRetrieverProtocol { 13 | 14 | private var userList = [TwitterUser]() 15 | 16 | public var resourceURL: (String, String) 17 | public var params: UsersSearchParams 18 | 19 | init(params: UsersSearchParams, resourceURL: (String, String)) { 20 | self.resourceURL = resourceURL 21 | self.params = params 22 | } 23 | 24 | public func fetchData(_ handler: @escaping (String, String, [TwitterUser]) -> Void) { 25 | if Constants.selfID != "-1" { 26 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 27 | 28 | client.getData() { data in 29 | if let data = data { 30 | 31 | let json = JSON(data: data) 32 | 33 | for (_, userJSON) in json { 34 | if userJSON.null == nil { 35 | let user = TwitterUser(with: userJSON) 36 | self.userList.append(user) // mem cycle? 37 | } 38 | } 39 | 40 | handler("0", "0", self.userList) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TweeBox/SimplePostParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimplePostParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SimplePostParams: ParamsProtocol { 12 | 13 | public var id: String 14 | 15 | // var trim_user = true 16 | 17 | 18 | init(id: String) { 19 | self.id = id 20 | } 21 | 22 | 23 | func getParams() -> [String : Any] { 24 | 25 | var params = [String: String]() 26 | 27 | params["id"] = id 28 | 29 | return params 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TweeBox/SimpleSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleSearchViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SimpleSearchViewController: UIViewController { 12 | 13 | public var keywordToSet: String? 14 | 15 | fileprivate var keyword: String? { 16 | return keywordToSet 17 | } 18 | 19 | fileprivate var fetchedUser: TwitterUser? 20 | 21 | @IBAction func findUser(_ sender: UIButton) { 22 | 23 | fetchUser() { [weak self] (user) in 24 | 25 | if let user = user { 26 | self?.fetchedUser = user 27 | self?.performSegue(withIdentifier: "Show User", sender: self) 28 | } else { 29 | self?.alertForNoSuchUser() 30 | } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /TweeBox/SimpleTweetComposer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleTweetComposer.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct SimpleTweetComposer { 13 | 14 | var id: String 15 | 16 | func retweet(handler: @escaping (Bool, Tweet?) -> Void) { 17 | 18 | let params = SimplePostParams(id: id) 19 | 20 | let dataRetriever = SimpleTweetPoster(params: params, resourceURL: ResourceURL.statuses_retweet_id) 21 | 22 | dataRetriever.postData { (retweet) in 23 | if self.id == retweet?.retweetedStatus?.id { 24 | handler(true, retweet) 25 | } else { 26 | handler(false, retweet) 27 | } 28 | } 29 | } 30 | 31 | 32 | func unRetweet(handler: @escaping (Bool, Tweet?) -> Void) { 33 | 34 | let params = SimplePostParams(id: id) 35 | 36 | let dataRetriever = SimpleTweetPoster(params: params, resourceURL: ResourceURL.statuses_unretweet_id) 37 | 38 | dataRetriever.postData { (retweet) in 39 | if self.id == retweet?.retweetedStatus?.id { 40 | handler(true, retweet) 41 | } else { 42 | handler(false, retweet) 43 | } 44 | } 45 | } 46 | 47 | 48 | func deleteTweet(handler: @escaping (Bool, Tweet?) -> Void) { 49 | 50 | let params = SimplePostParams(id: id) 51 | 52 | let dataRetriever = SimpleTweetPoster(params: params, resourceURL: ResourceURL.statuses_destroy_id) 53 | 54 | dataRetriever.postData { (tweet) in 55 | // if self.id == tweet?.id { 56 | // handler(true, tweet) 57 | // } else { 58 | // handler(false, tweet) 59 | // } 60 | } 61 | handler(true, nil) 62 | // FIX THIS! 63 | 64 | } 65 | 66 | 67 | func like(handler: @escaping (Bool, Tweet?) -> Void) { 68 | 69 | let params = SimplePostParams(id: id) 70 | 71 | let dataRetriever = FavoritePoster(params: params, resourceURL: ResourceURL.favorites_create) 72 | 73 | dataRetriever.postData { (tweet) in 74 | if self.id == tweet?.id { 75 | handler(true, tweet) 76 | } else { 77 | handler(false, tweet) 78 | } 79 | } 80 | } 81 | 82 | 83 | func dislike(handler: @escaping (Bool, Tweet?) -> Void) { 84 | 85 | let params = SimplePostParams(id: id) 86 | 87 | let dataRetriever = FavoritePoster(params: params, resourceURL: ResourceURL.favorites_destroy) 88 | 89 | dataRetriever.postData { (tweet) in 90 | if self.id == tweet?.id { 91 | handler(true, tweet) 92 | } else { 93 | handler(false, tweet) 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /TweeBox/SimpleTweetPoster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleTweetPoster.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | 13 | class SimpleTweetPoster { 14 | 15 | public var resourceURL: (String, String) 16 | public var params: SimplePostParams 17 | 18 | init(params: SimplePostParams, resourceURL: (String, String)) { 19 | self.resourceURL = resourceURL 20 | self.params = params 21 | } 22 | 23 | 24 | public func postData(_ handler: @escaping (Tweet?) -> Void) { 25 | if Constants.selfID != "-1" { 26 | let client = RESTfulClientWithID(resource: resourceURL, params: params.getParams()) 27 | 28 | print(">>> composer >> \(params.getParams())") 29 | 30 | client.getData() { data in 31 | if let data = data { 32 | let json = JSON(data: data) 33 | if json.null == nil { 34 | let tweet = Tweet(with: json) 35 | handler(tweet) 36 | } else { 37 | handler(nil) 38 | } 39 | } else { 40 | handler(nil) 41 | } 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /TweeBox/SimpleUserParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleUserParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SimpleUserParams: UserParams { 12 | 13 | // public var follow: Bool 14 | // Enable notifications for the target user. 15 | 16 | override func getParams() -> [String : Any] { 17 | 18 | var params = [String: String]() 19 | 20 | if userID != nil { 21 | params["user_id"] = userID 22 | } else if screenName != nil { 23 | params["screen_name"] = screenName 24 | } 25 | 26 | return params 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TweeBox/SingleTweet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleTweet.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/21. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | 13 | class SingleTweet { 14 | 15 | // private var user: TwitterUser! 16 | 17 | public var resourceURL: (String, String) 18 | public var params: TweetParams 19 | 20 | init(params: TweetParams, resourceURL: (String, String)) { 21 | self.resourceURL = resourceURL 22 | self.params = params 23 | } 24 | 25 | public func fetchData(_ handler: @escaping (Tweet?) -> Void) { 26 | if Constants.selfID != "-1" { 27 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 28 | 29 | client.getData() { data in 30 | if let data = data { 31 | let json = JSON(data: data) 32 | if json.null == nil { 33 | let tweet = Tweet(with: json) 34 | handler(tweet) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TweeBox/SingleUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleUser.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/20. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class SingleUser { 13 | 14 | // private var user: TwitterUser! 15 | 16 | public var resourceURL: (String, String) 17 | public var params: UserParams 18 | 19 | init(params: UserParams, resourceURL: (String, String)) { 20 | self.resourceURL = resourceURL 21 | self.params = params 22 | } 23 | 24 | public func fetchData(_ handler: @escaping (TwitterUser?) -> Void) { 25 | if Constants.selfID != "-1" { 26 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 27 | 28 | client.getData() { data in 29 | if let data = data { 30 | let json = JSON(data: data) 31 | if json.null == nil { 32 | let user = TwitterUser(with: json) 33 | handler(user) 34 | } else { 35 | handler(nil) 36 | } 37 | } else { 38 | handler(nil) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TweeBox/SpamUserParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpamUserParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/29. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SpamUserParams: SimpleUserParams { 12 | 13 | var performBlock = true 14 | 15 | init(userID: String?, screenName: String?, performBlock: Bool?) { 16 | 17 | if let performBlock = performBlock { 18 | self.performBlock = performBlock 19 | } 20 | 21 | super.init(userID: userID, screenName: screenName) 22 | 23 | } 24 | 25 | override func getParams() -> [String : Any] { 26 | 27 | var params = [String: String]() 28 | 29 | if userID != nil { 30 | params["user_id"] = userID 31 | } else if screenName != nil { 32 | params["screen_name"] = screenName 33 | } 34 | 35 | params["perform_block"] = performBlock.description 36 | 37 | return params 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /TweeBox/Timeline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timeline.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/28. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class Timeline { 13 | public var timeline = [Tweet]() 14 | public var maxID: String? 15 | public var sinceID: String? 16 | public var fetchNewer: Bool 17 | public var resourceURL: (String, String) 18 | public var params: ParamsWithBoundsProtocol 19 | 20 | init(maxID: String?, sinceID: String?, fetchNewer: Bool = true, resourceURL: (String, String), params: ParamsWithBoundsProtocol) { 21 | 22 | self.maxID = maxID 23 | self.sinceID = sinceID 24 | 25 | self.fetchNewer = fetchNewer 26 | 27 | self.resourceURL = resourceURL 28 | self.params = params 29 | } 30 | 31 | func appendTweet(from json: JSON) { 32 | 33 | for (_, tweetJSON) in json { 34 | if tweetJSON.null == nil { 35 | let tweet = Tweet(with: tweetJSON) 36 | self.timeline.append(tweet) // mem cycle? 37 | } 38 | } 39 | 40 | } 41 | 42 | public func fetchData(_ handler: @escaping (String?, String?, [Tweet]) -> Void) { 43 | 44 | if Constants.selfID != "-1" { 45 | 46 | if fetchNewer, sinceID != nil { 47 | params.sinceID = String(Int(sinceID!)! + 1) 48 | } 49 | 50 | if !fetchNewer, maxID != nil { 51 | params.maxID = String(Int(maxID!)! - 1) 52 | } 53 | 54 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 55 | print(">>> params \(params.getParams())") 56 | client.getData() { data in 57 | if let data = data { 58 | let json = JSON(data: data) 59 | 60 | self.appendTweet(from: json) 61 | 62 | self.maxID = self.timeline.last?.id // if fetch tweets below this batch, the earliest one in this batch would be the max one for the next batch 63 | self.sinceID = self.timeline.first?.id // vice versa 64 | 65 | handler(self.maxID, self.sinceID, self.timeline) 66 | } else { 67 | handler(self.maxID, self.sinceID, self.timeline) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /TweeBox/TimelineParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/9. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TimelineParams: ParamsWithBoundsProtocol { 12 | 13 | public var sinceID: String? 14 | 15 | // count: no need to define here 16 | 17 | public var maxID: String? 18 | 19 | // trimUser 20 | 21 | public var excludeReplies = false 22 | 23 | public var includeRetweets = true 24 | 25 | public var resourceURL = ResourceURL.search_tweets 26 | 27 | 28 | init(sinceID: String? = nil, maxID: String? = nil, excludeReplies: Bool?, includeRetweets: Bool?) { 29 | 30 | self.sinceID = sinceID 31 | self.maxID = maxID 32 | if let excludeReplies = excludeReplies { 33 | self.excludeReplies = excludeReplies 34 | } 35 | if let includeRetweets = includeRetweets { 36 | self.includeRetweets = includeRetweets 37 | } 38 | } 39 | 40 | public func getParams() -> [String: Any] { 41 | 42 | var params = [String: String]() 43 | 44 | params["count"] = Constants.tweetLimitPerRefresh 45 | 46 | if sinceID != nil { 47 | params["since_id"] = sinceID 48 | } 49 | 50 | if maxID != nil { 51 | params["max_id"] = maxID 52 | } 53 | 54 | if excludeReplies { 55 | params["exclude_replies"] = "true" 56 | } 57 | 58 | if !includeRetweets { 59 | params["include_rts"] = "false" 60 | } 61 | 62 | return params 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /TweeBox/TweeBox.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | TweeBox.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /TweeBox/TweeBox.xcdatamodeld/TweeBox.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /TweeBox/TweetClickableContentProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetClickableContentProtocol.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol TweetClickableContentProtocol { 13 | 14 | var keyword: String? { get set } 15 | var fetchedUser: TwitterUser? { get set } 16 | 17 | func performSegue(withIdentifier: String, sender: Any?) 18 | func alertForNoSuchUser(viewController: UIViewController) 19 | func setAndPerformSegueForHashtag() 20 | } 21 | -------------------------------------------------------------------------------- /TweeBox/TweetComposer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetComposer.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/30. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class TweetComposer { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /TweeBox/TweetEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetEntity.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/7. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class TweetEntity { 13 | 14 | var indices: [Int] 15 | 16 | init(with json: JSON) { 17 | indices = json["indices"].arrayValue.map({ $0.intValue }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TweeBox/TweetLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetLabel.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TweetLabel: YYLabel { 12 | 13 | /* 14 | // Only override draw() if you perform custom drawing. 15 | // An empty implementation adversely affects performance during animation. 16 | override func draw(_ rect: CGRect) { 17 | // Drawing code 18 | } 19 | */ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /TweeBox/TweetMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetMedia.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/7. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class TweetMedia: TweetEntity { 13 | 14 | public var id: String // id_str 15 | 16 | public var mediaURLHTTP: URL? // media_url 17 | public var mediaURL: URL? // media_url_https 18 | public var url: URL? 19 | // The media URL that was extracted 20 | public var displayURL: String 21 | // Not a URL but a string to display instead of the media URL 22 | public var expandedURL: URL? 23 | // The fully resolved media URL 24 | 25 | public var sizes: TweetPhotoSize 26 | /* 27 | We support different sizes: thumb, small, medium and large. 28 | The media_url defaults to medium but you can retrieve the media in different sizes by appending a colon + the size key 29 | (for example: http://pbs.twimg.com/media/A7EiDWcCYAAZT1D.jpg:thumb ). 30 | Each available size comes with three attributes that describe it: 31 | w : the width (in pixels) of the media in this particular size; 32 | h : the height (in pixels) of the media in this particular size; 33 | and resize : how we resized the media to this particular size (can be crop or fit ) 34 | */ 35 | 36 | public var type: String 37 | 38 | public var extAltText: String? 39 | 40 | public var sourceStatusID: String? 41 | // For Tweets containing media that was originally associated with a different tweet, 42 | // this string-based ID points to the original Tweet. 43 | 44 | // only for video 45 | public var videoInfo: TweetVideoInfo? 46 | 47 | 48 | init(with json: JSON, quality: MediaSize) { 49 | 50 | id = json["id_str"].stringValue 51 | mediaURLHTTP = URL(string: json["media_url"].stringValue, quality: quality) 52 | mediaURL = URL(string: json["media_url_https"].stringValue, quality: quality) 53 | url = URL(string: json["url"].stringValue) 54 | displayURL = json["display_url"].stringValue 55 | expandedURL = URL(string: json["expanded_url"].stringValue) 56 | sizes = TweetPhotoSize(with: json["sizes"]) 57 | type = json["type"].stringValue 58 | extAltText = json["ext_alt_text"].string 59 | sourceStatusID = json["source_status_id_str"].string 60 | 61 | if json["video_info"].null == nil { 62 | // video exists 63 | videoInfo = TweetVideoInfo(with: json["video_info"]) 64 | } 65 | 66 | super.init(with: json) 67 | } 68 | } 69 | 70 | extension TweetMedia { 71 | 72 | func getCutSize(with ratio: CGFloat, at quality: MediaSize) -> CGSize { 73 | 74 | let picWidth: CGFloat 75 | let picHeight: CGFloat 76 | 77 | let actualSize: TweetPhotoSize.sizeObject 78 | 79 | 80 | switch quality { 81 | case .thumb: 82 | actualSize = self.sizes.thumb 83 | case .small: 84 | actualSize = self.sizes.small 85 | case .medium: 86 | actualSize = self.sizes.medium 87 | case .large: 88 | actualSize = self.sizes.large 89 | default: 90 | actualSize = self.sizes.large 91 | } 92 | let actualHeight = CGFloat(actualSize.h) 93 | let actualWidth = CGFloat(actualSize.w) 94 | 95 | if actualHeight / actualWidth >= ratio { 96 | // too long 97 | picWidth = actualWidth 98 | picHeight = picWidth * ratio 99 | } else { 100 | // too wide 101 | picHeight = actualHeight 102 | picWidth = picHeight / ratio 103 | } 104 | 105 | return CGSize(width: picWidth, height: picHeight) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /TweeBox/TweetParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/21. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TweetParams: ParamsProtocol { 12 | 13 | public var tweetID: String? 14 | 15 | public var trimUser = false 16 | 17 | public var includeMyRetweet = true 18 | 19 | public var includeEntities = true 20 | 21 | public var includeExtAltText = true 22 | 23 | 24 | init(of tweetID: String) { self.tweetID = tweetID } 25 | 26 | 27 | public func getParams() -> [String: Any] { 28 | 29 | var params = [String: String]() 30 | 31 | guard tweetID != nil else { return params } 32 | 33 | params["id"] = tweetID! 34 | 35 | if trimUser { 36 | params["trim_user"] = "true" 37 | } 38 | 39 | if !includeMyRetweet { 40 | params["include_my_retweet"] = "false" 41 | } 42 | 43 | if !includeEntities { 44 | params["include_entities"] = "false" 45 | } 46 | 47 | if !includeExtAltText { 48 | params["include_ext_alt_text"] = "false" 49 | } 50 | 51 | return params 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /TweeBox/TweetPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetPhoto.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/7. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class TweetPhoto: TweetEntity { 13 | 14 | public var id: String 15 | 16 | public var media_url: String 17 | public var media_url_https: String 18 | public var url: String 19 | // The media URL that was extracted 20 | public var display_url: String 21 | // Not a URL but a string to display instead of the media URL 22 | public var expanded_url: String 23 | // The fully resolved media URL 24 | 25 | public var sizes: TweetMediaSize 26 | /* 27 | We support different sizes: thumb, small, medium and large. 28 | The media_url defaults to medium but you can retrieve the media in different sizes by appending a colon + the size key 29 | (for example: http://pbs.twimg.com/media/A7EiDWcCYAAZT1D.jpg:thumb ). 30 | Each available size comes with three attributes that describe it: 31 | w : the width (in pixels) of the media in this particular size; 32 | h : the height (in pixels) of the media in this particular size; 33 | and resize : how we resized the media to this particular size (can be crop or fit ) 34 | */ 35 | 36 | public var type: String 37 | 38 | public var ext_alt_text: String? 39 | 40 | public var source_status_id: String? 41 | // For Tweets containing media that was originally associated with a different tweet, 42 | // this string-based ID points to the original Tweet. 43 | 44 | 45 | // for video 46 | 47 | public var video_info: String? 48 | // Contains information about aspect ratio. 49 | // Typical values are [4, 3] or [16, 9]. 50 | // This field is present only when there is a video in the payload. 51 | 52 | public var duration_millis: Int? 53 | 54 | public var variants: 55 | 56 | } 57 | -------------------------------------------------------------------------------- /TweeBox/TweetPhotoSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetPhotoSize.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/7. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | struct TweetPhotoSize { 13 | 14 | public var thumb: sizeObject 15 | public var small: sizeObject 16 | public var medium: sizeObject 17 | public var large: sizeObject 18 | 19 | init(with json: JSON) { 20 | thumb = sizeObject(with: json["thumb"]) 21 | small = sizeObject(with: json["small"]) 22 | medium = sizeObject(with: json["medium"]) 23 | large = sizeObject(with: json["large"]) 24 | } 25 | 26 | struct sizeObject { 27 | 28 | public var w: Int 29 | public var h: Int 30 | public var resize: String 31 | 32 | init(with json: JSON) { 33 | w = json["w"].intValue 34 | h = json["h"].intValue 35 | resize = json["resize"].stringValue 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TweeBox/TweetPostParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetPostParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/30. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TweetPostParams: ParamsProtocol { 12 | 13 | var text: String // status 14 | 15 | var inReplyToStatusID: String? 16 | 17 | var possiblySensitive: Bool 18 | 19 | // lat 20 | // long 21 | // place_id 22 | // display_coordinates 23 | 24 | var trimUser = false 25 | 26 | var mediaIDs: String? // [String]() 27 | 28 | var enableDMCommands = false 29 | 30 | var failDMCommands = false 31 | 32 | 33 | init(text: String, inReplyToStatusID: String?, possiblySensitive: Bool, mediaIDs: String?) { 34 | self.text = text 35 | 36 | if let inReplyToStatusID = inReplyToStatusID { 37 | self.inReplyToStatusID = inReplyToStatusID 38 | } 39 | 40 | self.possiblySensitive = possiblySensitive 41 | 42 | if let mediaIDs = mediaIDs { 43 | self.mediaIDs = mediaIDs 44 | } 45 | } 46 | 47 | func getParams() -> [String : Any] { 48 | 49 | var params = [String : Any]() 50 | 51 | params["status"] = text 52 | 53 | if let inReplyToStatusID = inReplyToStatusID { 54 | params["in_reply_to_status_id"] = inReplyToStatusID 55 | } 56 | 57 | params["possibly_sensitive"] = possiblySensitive.description 58 | 59 | if let mediaIDs = mediaIDs { 60 | params["media_ids"] = mediaIDs 61 | } 62 | 63 | params["enable_dm_commands"] = enableDMCommands.description 64 | 65 | params["fail_dm_commands"] = failDMCommands.description 66 | 67 | return params 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /TweeBox/TweetPoster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetPoster.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/30. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | 13 | class TweetPoster { 14 | 15 | private let resourceURL = ResourceURL.statuses_update 16 | public var tweetParams: TweetPostParams 17 | 18 | 19 | init(tweetParams: TweetPostParams) { 20 | self.tweetParams = tweetParams 21 | } 22 | 23 | 24 | func postData(_ handler: @escaping (Tweet?) -> Void) { 25 | 26 | if Constants.selfID != "-1" { 27 | let client = RESTfulClient(resource: resourceURL, params: tweetParams.getParams()) 28 | 29 | print(">>> TweetPoster >> \(tweetParams.getParams())") 30 | 31 | client.getData() { data in 32 | if let data = data { 33 | let json = JSON(data: data) 34 | if json.null == nil { 35 | let tweet = Tweet(with: json) 36 | handler(tweet) 37 | } else { 38 | handler(nil) 39 | } 40 | } else { 41 | handler(nil) 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /TweeBox/TweetSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetSymbol.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //class TweetSymbol: Hashtag { } 12 | -------------------------------------------------------------------------------- /TweeBox/TweetTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetTableViewCell.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/8. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | import DateToolsSwift 12 | 13 | 14 | protocol TweetTableViewCellProtocol: class { 15 | 16 | func profileImageTapped(section: Int, row: Int) 17 | 18 | func imageTapped(section: Int, row: Int, mediaIndex: Int, media: [TweetMedia]) 19 | 20 | func originTweetTapped(section: Int, row: Int) 21 | } 22 | 23 | class TweetTableViewCell: UITableViewCell { 24 | 25 | // tap to segue 26 | weak var delegate: TweetTableViewCellProtocol? 27 | 28 | var section: Int? 29 | var row: Int? 30 | var mediaIndex: Int? 31 | 32 | var retweet: Tweet? 33 | 34 | var tweet: Tweet? { 35 | didSet { 36 | if let originTweet = tweet?.retweetedStatus, let retweetText = tweet?.text, retweetText.hasPrefix("RT @") { 37 | retweet = tweet 38 | tweet = originTweet 39 | } 40 | updateUI() 41 | } 42 | } 43 | 44 | 45 | @IBOutlet weak var tweetUserProfilePic: UIImageView! 46 | @IBOutlet weak var tweetCreatedTime: UILabel! 47 | @IBOutlet weak var tweetUserName: UILabel! 48 | @IBOutlet weak var tweetUserID: UILabel! 49 | 50 | @IBOutlet weak var retweetCount: UILabel! 51 | @IBOutlet weak var likeCount: UILabel! 52 | 53 | @IBOutlet weak var replyImage: UIImageView! 54 | @IBOutlet weak var retweetImage: UIImageView! 55 | @IBOutlet weak var retweeterProfileImage: UIImageView! 56 | @IBOutlet weak var likeImage: UIImageView! 57 | 58 | 59 | @IBAction func profileImageTapped(byReactingTo: UIGestureRecognizer) { 60 | guard let section = section, let row = row else { return } 61 | delegate?.profileImageTapped(section: section, row: row) 62 | } 63 | 64 | 65 | func updateUI() { 66 | 67 | // if let mainTweetID = tweet?.inReplyToStatusID { 68 | // self.backgroundColor = UIColor(red: 0, green: 1, blue: 0, alpha: 0.05) 69 | // } 70 | 71 | if let userProfileImageURL = tweet?.user.profileImageURL, let picView = self.tweetUserProfilePic { 72 | 73 | picView.kf.setImage( 74 | with: userProfileImageURL, 75 | // placeholder: placeholder, 76 | options: [ 77 | .transition(.fade(Constants.picFadeInDuration)), 78 | ] 79 | ) 80 | 81 | // picView.layer.borderWidth = 3.0 82 | // picView.layer.borderColor = UIColor.lightGray.cgColor 83 | 84 | picView.cutToRound(radius: nil) 85 | 86 | // tap to segue 87 | let tapOnProfileImage = UITapGestureRecognizer( 88 | target: self, 89 | action: #selector(profileImageTapped(byReactingTo:)) 90 | ) 91 | tapOnProfileImage.numberOfTapsRequired = 1 92 | picView.addGestureRecognizer(tapOnProfileImage) 93 | picView.isUserInteractionEnabled = true 94 | } else { 95 | tweetUserProfilePic?.image = nil 96 | } 97 | 98 | if let created = tweet?.createdTime { 99 | self.tweetCreatedTime?.text = created.shortTimeAgoSinceNow 100 | } else { 101 | tweetCreatedTime?.text = nil 102 | } 103 | 104 | tweetUserName.text = tweet?.user.name 105 | if let id = tweet?.user.screenName { 106 | tweetUserID.text = "@" + id 107 | } 108 | if (tweet?.inReplyToStatusID) != nil { 109 | replyImage.image = UIImage(named: "reply_true") 110 | } else { 111 | replyImage.image = UIImage(named: "reply_false") 112 | } 113 | 114 | 115 | let retweetedTweet = tweet?.retweeted ?? false 116 | let retweetedRetweet = retweet?.retweeted ?? false 117 | let retweeted = (retweetedTweet || retweetedRetweet) 118 | 119 | if retweeted { 120 | retweeterProfileImage?.removeFromSuperview() 121 | retweetImage.image = UIImage(named: "retweet_true") 122 | retweetCount.text = tweet?.retweetCount.shortExpression 123 | } else { 124 | retweetImage.image = UIImage(named: "retweet_false") 125 | 126 | if let retweet = retweet { 127 | 128 | retweetCount.text = retweet.user.name 129 | retweeterProfileImage?.kf.setImage(with: retweet.user.profileImageURL) 130 | retweeterProfileImage?.cutToRound(radius: 8) 131 | 132 | } else { 133 | retweeterProfileImage?.removeFromSuperview() 134 | retweetCount.text = tweet?.retweetCount.shortExpression 135 | } 136 | } 137 | 138 | likeCount.text = (tweet?.favoriteCount ?? 0).shortExpression 139 | 140 | 141 | let likedTweet = tweet?.favorited ?? false 142 | let likedRetweet = retweet?.favorited ?? false 143 | let liked = (likedTweet || likedRetweet) 144 | 145 | if liked { 146 | likeImage.image = UIImage(named: "like_true") 147 | } else { 148 | likeImage.image = UIImage(named: "like_false") 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /TweeBox/TweetURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetURL.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | class TweetURL: TweetEntity { 13 | 14 | var url: URL? 15 | // The t.co URL that was extracted from the Tweet text 16 | var displayURL: URL? 17 | var expandedURL: URL? 18 | // The resolved URL 19 | 20 | override init(with json: JSON) { 21 | 22 | url = URL(string: json["url"].stringValue) 23 | displayURL = URL(string: json["display_url"].stringValue) 24 | expandedURL = URL(string: json["expanded_url"].stringValue) 25 | 26 | super.init(with: json) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TweeBox/TweetVideoInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetVideoInfo.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/7. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | struct TweetVideoInfo { 13 | 14 | public var aspectRatio: [Int] 15 | // Contains information about aspect ratio. 16 | // Typical values are [4, 3] or [16, 9]. 17 | // This field is present only when there is a video in the payload. 18 | 19 | public var duration: Int? 20 | 21 | public var variants: [TweetVideoVariant]? 22 | 23 | init(with json: JSON) { 24 | aspectRatio = json["aspect_ratio"].arrayValue.map { $0.intValue } 25 | duration = json["duration_millis"].int 26 | variants = json["variants"].arrayValue.map { TweetVideoVariant(with: $0) } 27 | } 28 | 29 | struct TweetVideoVariant { 30 | 31 | public var bitrate: String 32 | public var contentType: String 33 | public var url: URL? 34 | 35 | init(with json: JSON) { 36 | bitrate = json["bitrate"].stringValue 37 | contentType = json["content_type"].stringValue 38 | url = URL(string: json["url"].stringValue) 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TweeBox/TweetWithPicAndTextTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetWithPicAndTextTableViewCell.swift 3 | // SmashTag 4 | // 5 | // Created by 4faramita on 2017/7/5. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import YYText 11 | 12 | 13 | class TweetWithPicAndTextTableViewCell: TweetWithPicTableViewCell { 14 | 15 | @IBOutlet weak var tweetTextContent: YYLabel! 16 | 17 | override func updateUI() { 18 | 19 | super.updateUI() 20 | 21 | if let tweet = tweet { 22 | tweetTextContent?.attributedText = TwitterAttributedContent(tweet).attributedString 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /TweeBox/TweetWithPicAndTextUsingButtonTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetWithPicAndTextUsingButtonTableViewCell.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/10. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | import SKPhotoBrowser 12 | 13 | class TweetWithPicAndTextUsingButtonTableViewCell: TweetTableViewCell { 14 | 15 | @IBOutlet weak var tweetPicContent: UIImageView! 16 | 17 | @IBOutlet weak var secondPic: UIImageView! 18 | 19 | @IBOutlet weak var thirdPic: UIImageView! 20 | 21 | @IBOutlet weak var fourthPic: UIImageView! 22 | 23 | private func setPic(at position: Int, of total: Int) { 24 | 25 | let media = tweet!.entities!.media! 26 | 27 | let pics = [tweetPicContent, secondPic, thirdPic, fourthPic] // pointer or copy? 28 | 29 | var aspect: CGFloat { 30 | if (total == 2) || (total == 3 && position == 0) { 31 | return Constants.thinAspect 32 | } else { 33 | return Constants.normalAspect 34 | } 35 | } 36 | 37 | let pic = media[position] 38 | let tweetPicURL = pic.mediaURL 39 | 40 | let picWidth: CGFloat 41 | let picHeight: CGFloat 42 | let cutPoint = CGPoint(x: 0.5, y: 0.5) 43 | // means cut from middle out 44 | 45 | let actualHeight = CGFloat(pic.sizes.small.h) 46 | let actualWidth = CGFloat(pic.sizes.small.w) 47 | 48 | if actualHeight / actualWidth >= aspect { 49 | // too long 50 | picWidth = actualWidth 51 | picHeight = picWidth * aspect 52 | } else { 53 | // too wide 54 | picHeight = actualHeight 55 | picWidth = picHeight / aspect 56 | } 57 | 58 | // Kingfisher 59 | let placeholder = UIImage(named: "picPlaceholder")!.kf.image(withRoundRadius: Constants.picCornerRadius, fit: CGSize(width: picWidth, height: picHeight)) 60 | 61 | let processor = CroppingImageProcessor(size: CGSize(width: picWidth, height: picHeight), anchor: cutPoint) >> RoundCornerImageProcessor(cornerRadius: Constants.picCornerRadius) 62 | 63 | if let picView = pics[position] { 64 | // picView.kf.indicatorType = .activity 65 | picView.kf.setImage( 66 | with: tweetPicURL, 67 | placeholder: placeholder, 68 | options: [.transition(.fade(Constants.picFadeInDuration)), .processor(processor)] 69 | ) 70 | } 71 | } 72 | 73 | override func updateUI() { 74 | 75 | super.updateUI() 76 | 77 | if let total = tweet?.entities?.media?.count { 78 | for i in 0..= 2 ? 1 : 0 38 | case (true, false): // down left 39 | mediaIndex = totalMediaNum == 4 ? 2 : 0 40 | case (false, false): // down right 41 | mediaIndex = ((totalMediaNum == 4) ? 3 : ((totalMediaNum == 3) ? 2 : ((totalMediaNum == 2) ? 1 : 0))) 42 | } 43 | 44 | 45 | guard let section = section, let row = row, let mediaIndex = mediaIndex else { return } 46 | delegate?.imageTapped(section: section, row: row, mediaIndex: mediaIndex, media: (tweet?.entities?.media)!) 47 | } 48 | 49 | 50 | @IBOutlet weak var tweetPicContent: UIImageView! 51 | 52 | @IBOutlet weak var secondPic: UIImageView! 53 | 54 | @IBOutlet weak var thirdPic: UIImageView! 55 | 56 | @IBOutlet weak var fourthPic: UIImageView! 57 | 58 | private func setPic(at position: Int, of total: Int) { 59 | 60 | totalMediaNum = total 61 | 62 | let media = tweet!.entities!.media! 63 | 64 | images = [tweetPicContent, secondPic, thirdPic, fourthPic] 65 | 66 | var ratio: CGFloat { 67 | if (total == 2) || (total == 3 && position == 0) { 68 | return Constants.thinAspectRatio 69 | } else { 70 | return Constants.normalAspectRatio 71 | } 72 | } 73 | 74 | let pic = media[position] 75 | let tweetPicURL = pic.mediaURL 76 | 77 | if pic.type != "photo" { 78 | 79 | let visualEffectView = VisualEffectView(frame: tweetPicContent.bounds) 80 | visualEffectView.blurRadius = 2 81 | visualEffectView.colorTint = .white 82 | visualEffectView.colorTintAlpha = 0.2 83 | tweetPicContent.addSubview(visualEffectView) 84 | 85 | let play = UIImageView(image: UIImage(named: "play_video")) 86 | tweetPicContent.addSubview(play) 87 | play.snp.makeConstraints { (make) in 88 | make.center.equalTo(tweetPicContent) 89 | make.height.equalTo(32) 90 | make.width.equalTo(32) 91 | } 92 | 93 | if pic.type == "animated_gif" { 94 | let gif = UIImageView(image: UIImage(named: "play_gif")) 95 | tweetPicContent.addSubview(gif) 96 | gif.snp.makeConstraints { (make) in 97 | make.trailing.equalTo(tweetPicContent).offset(-10) 98 | make.bottom.equalTo(tweetPicContent) 99 | make.height.equalTo(32) 100 | make.width.equalTo(32) 101 | } 102 | } 103 | } 104 | 105 | let cutSize = pic.getCutSize(with: ratio, at: .small) 106 | let cutPoint = CGPoint(x: 0.5, y: 0.5) 107 | // means cut from middle out 108 | 109 | // Kingfisher 110 | let placeholder = UIImage(named: "picPlaceholder")!.kf.image(withRoundRadius: Constants.picCornerRadius, fit: cutSize) 111 | 112 | let processor = CroppingImageProcessor(size: cutSize, anchor: cutPoint) 113 | 114 | if let picView = images[position] { 115 | // picView.kf.indicatorType = .activity 116 | picView.kf.setImage( 117 | with: tweetPicURL, 118 | placeholder: placeholder, 119 | options: [ 120 | .transition(.fade(Constants.picFadeInDuration)), 121 | .processor(processor) 122 | ] 123 | ) 124 | picView.layer.borderWidth = 1 125 | picView.layer.borderColor = UIColor.white.cgColor 126 | picView.cutToRound(radius: Constants.picCornerRadius) 127 | 128 | 129 | // tap to segue 130 | let ptap = UITapGestureRecognizer( 131 | target: self, 132 | action: #selector(imageTapped(byReactingTo:)) 133 | ) 134 | ptap.numberOfTapsRequired = 1 135 | picView.addGestureRecognizer(ptap) 136 | picView.isUserInteractionEnabled = true 137 | } 138 | } 139 | 140 | override func updateUI() { 141 | 142 | super.updateUI() 143 | 144 | if let total = tweet?.entities?.media?.count { 145 | for i in 0.. Void) { 49 | 50 | if Constants.selfID != "-1" { 51 | 52 | if fetchOlder, nextCursor != "0" { 53 | params.cursor = nextCursor 54 | } 55 | 56 | if !fetchOlder, previousCursor != "0" { 57 | params.cursor = previousCursor 58 | } 59 | 60 | 61 | let client = RESTfulClient(resource: resourceURL, params: params.getParams()) 62 | 63 | print(">>> params >> \(params.getParams())") 64 | 65 | client.getData() { data in 66 | if let data = data { 67 | let json = JSON(data: data) 68 | 69 | self.nextCursor = json["next_cursor_str"].stringValue 70 | self.previousCursor = json["previous_cursor_str"].stringValue 71 | 72 | for (_, userJSON) in json["users"] { 73 | if userJSON.null == nil { 74 | let user = TwitterUser(with: userJSON) 75 | self.userList.append(user) // mem cycle? 76 | } 77 | } 78 | 79 | if self.fetchOlder, let tailID = self.tailID { 80 | for index in stride(from: self.userList.endIndex - 1, through: self.userList.startIndex, by: -1) { 81 | if self.userList[index].id == tailID { 82 | // self.userList = self.userList[(index + 1)..<(self.userList.endIndex)] 83 | self.userList = Array(self.userList.suffix(self.userList.endIndex - index - 1)) 84 | break 85 | } 86 | } 87 | } 88 | 89 | if !(self.fetchOlder), let headID = self.headID { 90 | for index in self.userList.startIndex.. [String: Any] { 23 | 24 | var params = [String: String]() 25 | 26 | params["user_id"] = userID 27 | 28 | params["cursor"] = cursor 29 | 30 | params["count"] = count 31 | 32 | params["skip_status"] = skipStatus ? "true" : "false" 33 | 34 | params["include_user_entities"] = includeUserEntities ? "true" : "false" 35 | 36 | return params 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /TweeBox/UserListRetrieverProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserListRetrieverProtocol.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/26. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol UserListRetrieverProtocol { 12 | 13 | func fetchData(_ handler: @escaping (String, String, [TwitterUser]) -> Void) 14 | } 15 | -------------------------------------------------------------------------------- /TweeBox/UserParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/20. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class UserParams: ParamsProtocol { 12 | 13 | public var userID: String? 14 | 15 | public var screenName: String? 16 | 17 | public var includeEntities = true 18 | 19 | init(userID: String?, screenName: String?) { 20 | self.userID = userID 21 | self.screenName = screenName 22 | } 23 | 24 | 25 | public func getParams() -> [String: Any] { 26 | 27 | var params = [String: String]() 28 | 29 | guard (userID != nil || screenName != nil) else { 30 | return params 31 | } 32 | 33 | if userID != nil { 34 | params["user_id"] = userID 35 | } else { 36 | params["screen_name"] = screenName 37 | } 38 | 39 | params["include_entities"] = includeEntities.description 40 | 41 | return params 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TweeBox/UserProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfileViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/10. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TwitterKit 11 | 12 | class UserProfileViewController: UIViewController { 13 | 14 | public var userID: String? 15 | 16 | @IBOutlet weak var profilePic: UIImageView! 17 | @IBOutlet weak var nameTextLabel: UILabel! 18 | @IBOutlet weak var screenNameTextLabel: UILabel! 19 | 20 | override func viewDidLoad() { 21 | profilePic.image = UIImage(named: "picPlaceholder") 22 | nameTextLabel.text = "我的阿波罗鸡盒" 23 | screenNameTextLabel.text = "@4faramita" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TweeBox/UserTimelineParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTimelineParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/7/31. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class UserTimelineParams: TimelineParams { 12 | 13 | public var userID: String? 14 | 15 | // screenName 16 | 17 | init(of userID: String, sinceID: String? = nil, maxID: String? = nil, excludeReplies: Bool = false, includeRetweets: Bool = true) { 18 | 19 | self.userID = userID 20 | 21 | super.init(excludeReplies: nil, includeRetweets: nil) 22 | resourceURL = ResourceURL.statuses_user_timeline 23 | } 24 | 25 | override public func getParams() -> [String: Any] { 26 | var params = super.getParams() 27 | 28 | if userID != nil { 29 | params["user_id"] = userID 30 | } 31 | 32 | return params 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TweeBox/UsersSearchParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersSearchParams.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/26. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class UsersSearchParams: ParamsProtocol { 12 | 13 | public var query: String 14 | 15 | public var page: Int { 16 | // Specifies the page of results to retrieve. 17 | return Int(Int(Constants.tweetLimitPerRefresh)! / 20) 18 | } 19 | 20 | public var count = 20 21 | // The number of potential user results to retrieve per page. 22 | // This value has a maximum of 20. 23 | 24 | public var includeEntities = true 25 | 26 | 27 | init(query: String) { 28 | self.query = query 29 | } 30 | 31 | func getParams() -> [String : Any] { 32 | 33 | var params = [String: String]() 34 | 35 | params["q"] = query 36 | 37 | params["page"] = "\(page)" 38 | 39 | params["count"] = "\(count)" 40 | 41 | params["include_entities"] = includeEntities.description 42 | 43 | return params 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TweeBox/VideoViewerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoViewerViewController.swift 3 | // TweeBox 4 | // 5 | // Created by 4faramita on 2017/8/13. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import BMPlayer 11 | 12 | class VideoViewerViewController: PannableViewController { 13 | 14 | private var isInLandscapeMode: Bool! 15 | 16 | public var tweetMedia: TweetMedia! { 17 | didSet { 18 | // BMPlayerConf.enableBrightnessGestures = false 19 | // BMPlayerConf.enableVolumeGestures = false 20 | 21 | let player = BMPlayer() 22 | player.center = view.center 23 | view.addSubview(player) 24 | player.snp.makeConstraints { (make) in 25 | // make.centerY.equalTo(self.view.snp.centerY) 26 | // make.left.right.equalTo(self.view) 27 | make.center.equalTo(self.view) 28 | make.leading.greaterThanOrEqualTo(0) 29 | make.trailing.greaterThanOrEqualTo(0) 30 | make.top.greaterThanOrEqualTo(0) 31 | make.bottom.greaterThanOrEqualTo(0) 32 | let aspectRatio = CGFloat((tweetMedia.videoInfo?.aspectRatio[1])!) / CGFloat((tweetMedia.videoInfo?.aspectRatio[0])!) 33 | make.height.equalTo(player.snp.width).multipliedBy(aspectRatio).priority(750) 34 | } 35 | player.backBlock = { [unowned self] (wtf) in 36 | let _ = self.presentingViewController?.dismiss(animated: true) 37 | } 38 | 39 | 40 | let asset = BMPlayerResource(url: (tweetMedia.videoInfo?.variants?[0].url)!, name: tweetMedia.extAltText ?? "") 41 | player.setVideo(resource: asset) 42 | } 43 | } 44 | 45 | // override var prefersStatusBarHidden: Bool { 46 | // return true 47 | // } 48 | 49 | // override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { 50 | // return .slide 51 | // } 52 | 53 | override func viewWillAppear(_ animated: Bool) { 54 | let size: CGSize = UIScreen.main.bounds.size 55 | if size.width / size.height > 1 { 56 | isInLandscapeMode = true 57 | } else { 58 | isInLandscapeMode = false 59 | } 60 | } 61 | 62 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 63 | if (size.width / size.height > 1) { 64 | isInLandscapeMode = true 65 | } else { 66 | isInLandscapeMode = false 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /TweeBoxTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TweeBoxTests/TweeBoxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweeBoxTests.swift 3 | // TweeBoxTests 4 | // 5 | // Created by 4faramita on 2017/7/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TweeBox 11 | 12 | class TweeBoxTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /TweeBoxUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TweeBoxUITests/TweeBoxUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweeBoxUITests.swift 3 | // TweeBoxUITests 4 | // 5 | // Created by 4faramita on 2017/7/27. 6 | // Copyright © 2017年 4faramita. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class TweeBoxUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------