├── Hacker Swifter ├── Playground │ ├── .DS_Store │ └── scrapperPlayground.playground │ │ ├── section-1.swift │ │ ├── contents.xcplayground │ │ └── timeline.xctimeline ├── HackerSwifter.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcuserdata │ │ │ └── Dimillian.xcuserdatad │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── HackerSwifter.xccheckout │ ├── xcuserdata │ │ └── Dimillian.xcuserdatad │ │ │ └── xcschemes │ │ │ ├── xcschememanagement.plist │ │ │ └── Hacker Swifter.xcscheme │ └── project.pbxproj ├── Hacker Swifter │ ├── Hacker Swifter.h │ ├── Extensions │ │ ├── HTMLScanner.swift │ │ └── HTMLString.swift │ ├── Info.plist │ ├── HTTP │ │ ├── Fetcher.swift │ │ └── Cache.swift │ └── Models │ │ ├── Comment.swift │ │ └── Post.swift ├── Hacker SwifterTests │ ├── Info.plist │ ├── FetcherTests.swift │ ├── Hacker_SwifterTests.swift │ ├── CommentTests.swift │ ├── CacheTests.swift │ └── PostTests.swift └── HackerSwifter copy-Info.plist ├── HackerSwifter.podspec ├── .gitignore ├── LICENSE └── README.md /Hacker Swifter/Playground/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/HackerSwifter/HEAD/Hacker Swifter/Playground/.DS_Store -------------------------------------------------------------------------------- /Hacker Swifter/Playground/scrapperPlayground.playground/section-1.swift: -------------------------------------------------------------------------------- 1 | // Playground - noun: a place where people can play 2 | 3 | import Cocoa 4 | 5 | var session = NSURLSession.dataTaskWithURL() 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Hacker Swifter/Playground/scrapperPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter.xcodeproj/project.xcworkspace/xcuserdata/Dimillian.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 6 | 7 | SnapshotAutomaticallyBeforeSignificantChanges 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Hacker Swifter/Playground/scrapperPlayground.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /HackerSwifter.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "HackerSwifter" 3 | s.version = "0.1.0" 4 | s.summary = "A Hacker News Swift library" 5 | s.homepage = "https://github.com/Dimillian/HackerSwifter" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Thomas Ricouard" => "ricouard77@gmail.com" } 8 | s.source = { :git => "https://github.com/Dimillian/HackerSwifter.git", :tag => "0.1.0" } 9 | s.source_files = "Hacker Swifter/Hacker Swifter/**/*.{h,swift}", "Hacker Swifter/Hacker Swifter/*.{h}" 10 | s.frameworks = "Foundation" 11 | s.requires_arc = true 12 | end -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/Hacker Swifter.h: -------------------------------------------------------------------------------- 1 | // 2 | // Hacker Swifter.h 3 | // Hacker Swifter 4 | // 5 | // Created by Thomas Ricouard on 10/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Hacker Swifter. 12 | FOUNDATION_EXPORT double Hacker_SwifterVersionNumber; 13 | 14 | //! Project version string for Hacker Swifter. 15 | FOUNDATION_EXPORT const unsigned char Hacker_SwifterVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/Extensions/HTMLScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLScanner.swift 3 | // Hacker Swifter 4 | // 5 | // Created by Thomas Ricouard on 11/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSScanner { 12 | func scanTag(startTag: String, endTag: String) -> String { 13 | var temp: NSString? = "" 14 | var result: NSString? = "" 15 | self.scanUpToString(startTag, intoString: &temp) 16 | self.scanString(startTag, intoString: &temp) 17 | self.scanUpToString(endTag, intoString: &result) 18 | return result as! String 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | ### Objective-C ### 10 | # Xcode 11 | # 12 | build/ 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | *.xccheckout 23 | *.moved-aside 24 | DerivedData 25 | *.hmap 26 | *.ipa 27 | *.xcuserstate 28 | *.xcuserdatad 29 | 30 | *.DS_Store 31 | 32 | *.xcbkptlist 33 | 34 | *.xcuserstate 35 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker SwifterTests/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter copy-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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter.xcodeproj/xcuserdata/Dimillian.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Hacker Swifter.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | HackerSwifter copy.xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 9FD4A195196F3007003608F6 21 | 22 | primary 23 | 24 | 25 | 9FD4A1A0196F3008003608F6 26 | 27 | primary 28 | 29 | 30 | 9FE8C8111C2A9B1D001D91EF 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker SwifterTests/FetcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fetcher.swift 3 | // Hacker Swifter 4 | // 5 | // Created by Thomas Ricouard on 11/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class FetcherTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | XCTAssert(true, "Pass") 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | self.measureBlock() { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker SwifterTests/Hacker_SwifterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hacker_SwifterTests.swift 3 | // Hacker SwifterTests 4 | // 5 | // Created by Thomas Ricouard on 10/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Hacker_SwifterTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | XCTAssert(true, "Pass") 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | self.measureBlock() { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Thomas Ricouard 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HackerSwifter 2 | ============= 3 | 4 | A Swift Hacker News library for iOS and OSX 5 | 6 | ## Goal 7 | 8 | We want to make a new shiny and powerful library for scrapping [Hacker News](https://news.ycombinator.com). Kinda like [LibHN](https://github.com/bennyguitar/libHN) do in Objective-c. But HackerSwifter will add a lot of other features. 9 | 10 | 11 | ## Limitation 12 | 13 | As you know, Hacker News does not provide any official API, so we rely on scrapping the HTML pages in order to convert them into Swift object. This is the only solution at the moment, so things may broke and may not be future proof. 14 | 15 | But hey, the goal is to have a nice and clean library you can plug in your Swift projects. 16 | 17 | ## Features 18 | 19 | * Fetch the different feed pages (news, jobs, ask...) 20 | * Upvote post 21 | * Login 22 | * Fetch users page 23 | * Fetch comments 24 | * Post comments 25 | * Vote comments 26 | * HN Logic (500 karma comments vote etc...) 27 | * Provide a clear user facing error message 28 | * **Caching mechanism** for offline use 29 | * Full Swift 30 | * Less code possible 31 | * Offer a very clear and consise API 32 | * Easily manageable 33 | * Inteligent scrapping? 34 | 35 | ## Tech 36 | 37 | We will use `NSURLSession` and no fancy external library. 38 | 39 | Each models (Post, User, etc...) will directly expose class method to load itself or a list of itself, exemple. 40 | 41 | `Post.Load(.News, completionClosure([Post]: posts))` 42 | `User.load("username", completionClosure(User: user))` 43 | 44 | So no webservice or manager exposed, everything is done at the model level. 45 | 46 | More to come... 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter.xcodeproj/project.xcworkspace/xcshareddata/HackerSwifter.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | C22DDAF4-5499-4DF0-8E1F-80B1908A662C 9 | IDESourceControlProjectName 10 | HackerSwifter 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | E0D52B920DCA102ACF12EC644CE806103E82A316 14 | https://github.com/Dimillian/HackerSwifter.git 15 | 16 | IDESourceControlProjectPath 17 | Hacker Swifter/HackerSwifter.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | E0D52B920DCA102ACF12EC644CE806103E82A316 21 | ../../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/Dimillian/HackerSwifter.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | E0D52B920DCA102ACF12EC644CE806103E82A316 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | E0D52B920DCA102ACF12EC644CE806103E82A316 36 | IDESourceControlWCCName 37 | HackerSwifter 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker SwifterTests/CommentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentTests.swift 3 | // HackerSwifter 4 | // 5 | // Created by Tosin Afolabi on 17/07/2014. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import HackerSwifter 11 | 12 | class CommentTests: 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 testFetchComments() { 25 | let expectation = self.expectationWithDescription("fetch comments for post") 26 | 27 | let post = Post() 28 | post.postId = "8255637" 29 | post.type = Post.PostFilter.Default 30 | 31 | Comment.fetch(forPost: post, completion: {(comments: [Comment]!, error: Fetcher.ResponseError!, local: Bool) in 32 | 33 | if (!local) { 34 | XCTAssertTrue(comments!.count > 0, "comments should not be empty") 35 | expectation.fulfill() 36 | } 37 | }) 38 | 39 | self.waitForExpectationsWithTimeout(5.0, handler: nil) 40 | } 41 | 42 | func testAskHNComments() { 43 | let expectation = self.expectationWithDescription("fetch comments for post") 44 | 45 | let post = Post() 46 | post.postId = "8044029" 47 | post.type = Post.PostFilter.Ask 48 | 49 | Comment.fetch(forPost: post, completion: {(comments: [Comment]!, error: Fetcher.ResponseError!, local: Bool) in 50 | 51 | if (!local) { 52 | XCTAssertTrue(comments!.count > 0, "comments should not be empty") 53 | let comment = comments[0] 54 | XCTAssertTrue(comment.type == Comment.CommentFilter.Ask, "comment type is not good") 55 | XCTAssertTrue(comment.text?.utf8.count > 0, "Comment content should not be empty") 56 | expectation.fulfill() 57 | } 58 | }) 59 | 60 | self.waitForExpectationsWithTimeout(5.0, handler: nil) 61 | } 62 | 63 | 64 | 65 | func testPerformanceExample() { 66 | // This is an example of a performance test case. 67 | self.measureBlock() { 68 | // Put the code you want to measure the time of here. 69 | } 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/Extensions/HTMLString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLString.swift 3 | // HackerSwifter 4 | // 5 | // Created by Thomas Ricouard on 18/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | static func stringByRemovingHTMLEntities(string: String) -> String { 13 | var result = string 14 | 15 | result = result.stringByReplacingOccurrencesOfString("

", withString: "\n\n", options: .CaseInsensitiveSearch, range: nil) 16 | result = result.stringByReplacingOccurrencesOfString("

", withString: "", options: .CaseInsensitiveSearch, range: nil) 17 | result = result.stringByReplacingOccurrencesOfString("", withString: "", options: .CaseInsensitiveSearch, range: nil) 18 | result = result.stringByReplacingOccurrencesOfString("", withString: "", options: .CaseInsensitiveSearch, range: nil) 19 | result = result.stringByReplacingOccurrencesOfString("&", withString: "&", options: .CaseInsensitiveSearch, range: nil) 20 | result = result.stringByReplacingOccurrencesOfString(">", withString: ">", options: .CaseInsensitiveSearch, range: nil) 21 | result = result.stringByReplacingOccurrencesOfString("'", withString: "'", options: .CaseInsensitiveSearch, range: nil) 22 | result = result.stringByReplacingOccurrencesOfString("/", withString: "/", options: .CaseInsensitiveSearch, range: nil) 23 | result = result.stringByReplacingOccurrencesOfString(""", withString: "\"", options: .CaseInsensitiveSearch, range: nil) 24 | result = result.stringByReplacingOccurrencesOfString("<", withString: "<", options: .CaseInsensitiveSearch, range: nil) 25 | result = result.stringByReplacingOccurrencesOfString("<", withString: "<", options: .CaseInsensitiveSearch, range: nil) 26 | result = result.stringByReplacingOccurrencesOfString(">", withString: ">", options: .CaseInsensitiveSearch, range: nil) 27 | result = result.stringByReplacingOccurrencesOfString("&", withString: "&", options: .CaseInsensitiveSearch, range: nil) 28 | result = result.stringByReplacingOccurrencesOfString("
", withString: "", options: .CaseInsensitiveSearch, range: nil)
29 |         result = result.stringByReplacingOccurrencesOfString("
", withString: "", options: .CaseInsensitiveSearch, range: nil) 30 | 31 | let regex = try? NSRegularExpression(pattern: "]+href=\"(.*?)\"[^>]*>.*?", 32 | options: NSRegularExpressionOptions.CaseInsensitive) 33 | result = regex!.stringByReplacingMatchesInString(result, options: [], range: NSMakeRange(0, result.utf16.count), withTemplate: "$1") 34 | 35 | return result 36 | } 37 | } -------------------------------------------------------------------------------- /Hacker Swifter/Hacker SwifterTests/CacheTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheTests.swift 3 | // HackerSwifter 4 | // 5 | // Created by Thomas Ricouard on 16/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import HackerSwifter 11 | 12 | class CacheTests: 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 | func pathtest() { 24 | let path = "test/test?test/test" 25 | let result = "test#test?test#test" 26 | 27 | XCTAssertTrue(DiskCache.generateCacheKey(path) == result, "cache key is not equal to result") 28 | } 29 | 30 | func testMemoryCache() { 31 | let post = Post() 32 | post.title = "Test" 33 | 34 | MemoryCache.sharedMemoryCache.setObject(post, key: "post") 35 | 36 | let postTest = MemoryCache.sharedMemoryCache.objectForKeySync("post") as! Post 37 | 38 | XCTAssertNotNil(postTest, "Post is nil") 39 | XCTAssertTrue(postTest.isKindOfClass(Post), "Post is not kind of class post") 40 | XCTAssertTrue(postTest.title == "Test", "Post title is not equal to prior test") 41 | 42 | MemoryCache.sharedMemoryCache.removeObject("post") 43 | 44 | XCTAssertNil(MemoryCache.sharedMemoryCache.objectForKeySync("post"), "post should be nil") 45 | } 46 | 47 | func testDiskCache() { 48 | let post = Post() 49 | post.title = "Test" 50 | 51 | DiskCache.sharedDiskCache.setObject(post, key: "post") 52 | 53 | let postTest = DiskCache.sharedDiskCache.objectForKeySync("post") as! Post 54 | 55 | XCTAssertNotNil(postTest, "Post is nil") 56 | XCTAssertTrue(postTest.isKindOfClass(Post), "Post is not kind of class post") 57 | XCTAssertTrue(postTest.title == "Test", "Post title is not equal to prior test") 58 | 59 | DiskCache.sharedDiskCache.removeObject("post") 60 | 61 | XCTAssertNil(DiskCache.sharedDiskCache.objectForKeySync("post"), "post should be nil") 62 | } 63 | 64 | func testGlobalCache() { 65 | let post = Post() 66 | post.title = "Global Test" 67 | 68 | Cache.sharedCache.setObject(post, key: "post") 69 | 70 | let globalPost = Cache.sharedCache.objectForKeySync("post") as! Post 71 | let memoryPost = MemoryCache.sharedMemoryCache.objectForKeySync("post") as! Post 72 | let diskPost = DiskCache.sharedDiskCache.objectForKeySync("post") as! Post 73 | 74 | XCTAssertNotNil(globalPost, "Global Post is nil") 75 | XCTAssertNotNil(memoryPost, "Memory Post is nil") 76 | XCTAssertNotNil(diskPost, "Dissk Post is nil") 77 | 78 | XCTAssertTrue(globalPost.isKindOfClass(Post), "Global Post is not kind of class post") 79 | XCTAssertTrue(globalPost.title == "Global Test", "Global Post title is not equal to prior test") 80 | 81 | XCTAssertTrue(memoryPost.isKindOfClass(Post), "Memory Post is not kind of class post") 82 | XCTAssertTrue(memoryPost.title == "Global Test", "Memory Post title is not equal to prior test") 83 | 84 | XCTAssertTrue(diskPost.isKindOfClass(Post), "Disk Post is not kind of class post") 85 | XCTAssertTrue(diskPost.title == "Global Test", "Disk Post title is not equal to prior test") 86 | 87 | Cache.sharedCache.removeObject("post") 88 | 89 | XCTAssertNil(Cache.sharedCache.objectForKeySync("post"), "post should be nil") 90 | XCTAssertNil(MemoryCache.sharedMemoryCache.objectForKeySync("post"), "post should be nil") 91 | XCTAssertNil(DiskCache.sharedDiskCache.objectForKeySync("post"), "post should be nil") 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter.xcodeproj/xcuserdata/Dimillian.xcuserdatad/xcschemes/Hacker Swifter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker SwifterTests/PostTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // Hacker Swifter 4 | // 5 | // Created by Thomas Ricouard on 11/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import HackerSwifter 11 | 12 | class PostTests: 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 testFetchNews() { 25 | let expectation = self.expectationWithDescription("fetch posts") 26 | 27 | Post.fetch(.Top, completion: {(posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) in 28 | if (!local) { 29 | XCTAssertTrue(posts!.count > 1, "posts should contain post") 30 | expectation.fulfill() 31 | } 32 | }) 33 | 34 | self.waitForExpectationsWithTimeout(5.0, handler: nil) 35 | } 36 | 37 | func testFetchNewsPage2() { 38 | let expectation = self.expectationWithDescription("fetch posts") 39 | var postsPage1:[Post] = [] 40 | var postsPage2:[Post] = [] 41 | 42 | Post.fetch(.Top, page:1, completion: {(posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) in 43 | if (!local) { 44 | postsPage1 = posts 45 | XCTAssertTrue(posts!.count > 1, "page 1 posts should contain post") 46 | Post.fetch(.Top, page:2, completion: {(posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) in 47 | if (!local) { 48 | postsPage2 = posts 49 | XCTAssertTrue(posts!.count > 1, "page 2 posts should contain post") 50 | XCTAssertNotEqual(postsPage1[0], postsPage2[0], "page 1 and two have the same content") 51 | XCTAssertNotEqual(postsPage1[1], postsPage2[1], "page 1 and two have the same content") 52 | XCTAssertNotEqual(postsPage1[2], postsPage2[2], "page 1 and two have the same content") 53 | expectation.fulfill() 54 | } 55 | }) 56 | } 57 | }) 58 | self.waitForExpectationsWithTimeout(10.0, handler: nil) 59 | } 60 | 61 | func testFetchPostForUser() { 62 | let expectation = self.expectationWithDescription("fetch posts") 63 | 64 | Post.fetch("dimillian", completion: {(posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) in 65 | if (!local) { 66 | XCTAssertTrue(posts!.count > 1, "posts should contain post") 67 | expectation.fulfill() 68 | } 69 | }) 70 | 71 | self.waitForExpectationsWithTimeout(5.0, handler: nil) 72 | } 73 | 74 | func testFetchPostForUserPage2() { 75 | let expectation = self.expectationWithDescription("fetch posts") 76 | var postsPage1:[Post] = [] 77 | var postsPage2:[Post] = [] 78 | 79 | Post.fetch("antr", page:1, lastPostId: nil, completion: {(posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) in 80 | if (!local) { 81 | postsPage1 = posts 82 | XCTAssertTrue(posts!.count > 1, "page 1 posts should contain post") 83 | Post.fetch("antr", page:2, lastPostId:(postsPage1[postsPage1.count - 1]).postId, completion: {(posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) in 84 | if (!local) { 85 | postsPage2 = posts 86 | XCTAssertTrue(posts!.count > 1, "page 2 posts should contain post") 87 | XCTAssertNotEqual(postsPage1[0], postsPage2[0], "page 1 and two have the same content") 88 | XCTAssertNotEqual(postsPage1[1], postsPage2[1], "page 1 and two have the same content") 89 | XCTAssertNotEqual(postsPage1[2], postsPage2[2], "page 1 and two have the same content") 90 | expectation.fulfill() 91 | } 92 | }) 93 | } 94 | }) 95 | self.waitForExpectationsWithTimeout(10.0, handler: nil) 96 | } 97 | 98 | 99 | func testFetchPostsAPI() { 100 | let expectation = self.expectationWithDescription("fetch post") 101 | Post.fetchPost { (post, error, local) -> Void in 102 | if (!local) { 103 | XCTAssertTrue(post.count > 1, "API response should countain Post") 104 | Post.fetchPost(post[0], completion: { (post, error, local) -> Void in 105 | XCTAssertTrue(post.title?.utf8.count > 0, "Title content should not be empty") 106 | expectation.fulfill() 107 | }) 108 | } 109 | } 110 | self.waitForExpectationsWithTimeout(10.0, handler: nil) 111 | } 112 | 113 | func testPerformanceExample() { 114 | // This is an example of a performance test case. 115 | self.measureBlock() { 116 | // Put the code you want to measure the time of here. 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/HTTP/Fetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fetcher.swift 3 | // Hacker Swifter 4 | // 5 | // Created by Thomas Ricouard on 11/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let _Fetcher = Fetcher() 12 | 13 | public class Fetcher { 14 | 15 | internal let baseURL = "https://news.ycombinator.com/" 16 | internal let APIURL = "https://hacker-news.firebaseio.com/v0/" 17 | internal let APIFormat = ".json" 18 | private let session = NSURLSession.sharedSession() 19 | 20 | public typealias FetchCompletion = (object: AnyObject!, error: ResponseError!, local: Bool) -> Void 21 | public typealias FetchParsing = (html: String!) -> AnyObject! 22 | public typealias FetchParsingAPI = (json: AnyObject) -> AnyObject! 23 | 24 | public enum ResponseError: String { 25 | case NoConnection = "You are not connected to the internet" 26 | case ErrorParsing = "An error occurred while fetching the requested page" 27 | case UnknownError = "An unknown error occurred" 28 | } 29 | 30 | public enum APIEndpoint: String { 31 | case Post = "item/" 32 | case User = "user/" 33 | case Top = "topstories" 34 | case New = "newstories" 35 | case Ask = "askstories" 36 | case Jobs = "jobstories" 37 | case Show = "showstories" 38 | } 39 | 40 | public class var sharedInstance: Fetcher { 41 | return _Fetcher 42 | } 43 | 44 | class func Fetch(ressource: String, parsing: FetchParsing, completion: FetchCompletion) { 45 | 46 | let cacheKey = Cache.generateCacheKey(ressource) 47 | Cache.sharedCache.objectForKey(cacheKey, completion: {(object: AnyObject!) in 48 | if let realObject: AnyObject = object { 49 | completion(object: realObject, error: nil, local: true) 50 | } 51 | }) 52 | 53 | let path = _Fetcher.baseURL + ressource 54 | let task = _Fetcher.session.dataTaskWithURL(NSURL(string: path)! , completionHandler: {(data: NSData?, response, error: NSError?) in 55 | if !(error != nil) { 56 | if let realData = data { 57 | let object: AnyObject! = parsing(html: NSString(data: realData, encoding: NSUTF8StringEncoding) as! String) 58 | if let realObject: AnyObject = object { 59 | Cache.sharedCache.setObject(realObject, key: cacheKey) 60 | } 61 | dispatch_async(dispatch_get_main_queue(), { ()->() in 62 | completion(object: object, error: nil, local: false) 63 | }) 64 | } 65 | else { 66 | dispatch_async(dispatch_get_main_queue(), { ()->() in 67 | completion(object: nil, error: ResponseError.UnknownError, local: false) 68 | }) 69 | } 70 | } 71 | else { 72 | completion(object: nil, error: ResponseError.UnknownError, local: false) 73 | } 74 | }) 75 | task.resume() 76 | } 77 | 78 | //In the future, all scraping will be removed and we'll use only the Algolia API 79 | //At the moment this function is sufixed for testing purpose 80 | class func FetchJSON(endpoint: APIEndpoint, ressource: String?, parsing: FetchParsingAPI, completion: FetchCompletion) { 81 | var path: String 82 | if let realRessource: String = ressource { 83 | path = _Fetcher.APIURL + endpoint.rawValue + realRessource + _Fetcher.APIFormat 84 | } 85 | else { 86 | path = _Fetcher.APIURL + endpoint.rawValue + _Fetcher.APIFormat 87 | } 88 | 89 | let cacheKey = Cache.generateCacheKey(path) 90 | Cache.sharedCache.objectForKey(cacheKey, completion: {(object: AnyObject!) in 91 | if let realObject: AnyObject = object { 92 | completion(object: realObject, error: nil, local: true) 93 | } 94 | }) 95 | 96 | let task = _Fetcher.session.dataTaskWithURL(NSURL(string: path)! , completionHandler: {(data: NSData?, response, error: NSError?) in 97 | if let data = data { 98 | var error: NSError? = nil 99 | var JSON: AnyObject! 100 | do { 101 | JSON = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) 102 | } catch let error1 as NSError { 103 | error = error1 104 | JSON = nil 105 | } catch { 106 | fatalError() 107 | } 108 | if error == nil { 109 | let object: AnyObject! = parsing(json: JSON) 110 | if let object: AnyObject = object { 111 | if let realObject: AnyObject = object { 112 | Cache.sharedCache.setObject(realObject, key: cacheKey) 113 | } 114 | dispatch_async(dispatch_get_main_queue(), { ()->() in 115 | completion(object: object, error: nil, local: false) 116 | }) 117 | 118 | } 119 | else { 120 | dispatch_async(dispatch_get_main_queue(), { ()->() in 121 | completion(object: nil, error: ResponseError.ErrorParsing, local: false) 122 | }) 123 | 124 | } 125 | } 126 | else { 127 | completion(object: nil, error: ResponseError.UnknownError, local: false) 128 | } 129 | } 130 | }) 131 | task.resume() 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/HTTP/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // HackerSwifter 4 | // 5 | // Created by Thomas Ricouard on 16/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let _Cache = Cache() 12 | private let _MemoryCache = MemoryCache() 13 | private let _DiskCache = DiskCache() 14 | 15 | public typealias cacheCompletion = (AnyObject!) -> Void 16 | 17 | public class Cache { 18 | 19 | public class var sharedCache: Cache { 20 | return _Cache 21 | } 22 | 23 | init() { 24 | 25 | } 26 | 27 | public class func generateCacheKey(path: String) -> String { 28 | if (path == "") { 29 | return "root" 30 | } 31 | return path.stringByReplacingOccurrencesOfString("/", 32 | withString: "#", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil) 33 | } 34 | 35 | public func setObject(object: AnyObject, key: String) { 36 | if (object.conformsToProtocol(NSCoding)) { 37 | MemoryCache.sharedMemoryCache.setObject(object, key: key) 38 | DiskCache.sharedDiskCache.setObject(object, key: key) 39 | } 40 | } 41 | 42 | public func objectForKey(key: String, completion: cacheCompletion) { 43 | MemoryCache.sharedMemoryCache.objectForKey(key, completion: {(object: AnyObject!) in 44 | if let realObject: AnyObject = object { 45 | completion(realObject) 46 | } 47 | else { 48 | DiskCache.sharedDiskCache.objectForKey(key, completion: {(object: AnyObject!) in 49 | completion(object) 50 | }) 51 | } 52 | }) 53 | } 54 | 55 | public func objectForKeySync(key: String) -> AnyObject! { 56 | let ramObject: AnyObject! = MemoryCache.sharedMemoryCache.objectForKeySync(key) 57 | return (ramObject != nil) ? ramObject : DiskCache.sharedDiskCache.objectForKeySync(key) 58 | } 59 | 60 | public func removeObject(key: String) { 61 | MemoryCache.sharedMemoryCache.removeObject(key) 62 | DiskCache.sharedDiskCache.removeObject(key) 63 | } 64 | 65 | public func removeAllObject() { 66 | MemoryCache.sharedMemoryCache.removeAllObject() 67 | DiskCache.sharedDiskCache.removeAllObject() 68 | } 69 | } 70 | 71 | public class DiskCache: Cache { 72 | 73 | private struct files { 74 | static var filepath: String { 75 | let manager = NSFileManager.defaultManager() 76 | var paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.CachesDirectory, 77 | NSSearchPathDomainMask.UserDomainMask, true) 78 | let cachePath = paths[0] + "/modelCache/" 79 | if (!manager.fileExistsAtPath(cachePath)) { 80 | do { 81 | try manager.createDirectoryAtPath(cachePath, withIntermediateDirectories: true, attributes: nil) 82 | } catch _ { 83 | } 84 | } 85 | return cachePath 86 | } 87 | } 88 | 89 | private let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT 90 | 91 | public class var sharedDiskCache: Cache { 92 | return _DiskCache 93 | } 94 | 95 | override init() { 96 | 97 | } 98 | 99 | public func fullPath(key: String) -> String { 100 | return files.filepath + key 101 | } 102 | 103 | public func objectExist(key: String) -> Bool { 104 | return NSFileManager.defaultManager().fileExistsAtPath(fullPath(key)) 105 | } 106 | 107 | public override func objectForKey(key: String, completion: cacheCompletion) { 108 | if (self.objectExist(key)) { 109 | dispatch_async(dispatch_get_global_queue(self.priority, UInt(0)), { ()->() in 110 | let object: AnyObject! = NSKeyedUnarchiver.unarchiveObjectWithFile(self.fullPath(key)) 111 | dispatch_async(dispatch_get_main_queue(), { ()->() in 112 | completion(object) 113 | }) 114 | }) 115 | } 116 | else { 117 | completion(nil) 118 | } 119 | } 120 | 121 | public override func objectForKeySync(key: String) -> AnyObject! { 122 | if (self.objectExist(key)) { 123 | return NSKeyedUnarchiver.unarchiveObjectWithFile(self.fullPath(key)) 124 | } 125 | return nil 126 | } 127 | 128 | public override func setObject(object: AnyObject, key: String) { 129 | NSKeyedArchiver.archiveRootObject(object, toFile: self.fullPath(key)) 130 | } 131 | 132 | public override func removeObject(key: String) { 133 | if (self.objectExist(key)) { 134 | do { 135 | try NSFileManager.defaultManager().removeItemAtPath(self.fullPath(key)) 136 | } catch _ { 137 | } 138 | } 139 | } 140 | 141 | public override func removeAllObject() { 142 | 143 | } 144 | } 145 | 146 | public class MemoryCache: Cache { 147 | 148 | private var memoryCache = NSCache() 149 | 150 | public class var sharedMemoryCache: Cache { 151 | return _MemoryCache 152 | } 153 | 154 | override init() { 155 | 156 | } 157 | 158 | public override func objectForKeySync(key: String) -> AnyObject! { 159 | return self.memoryCache.objectForKey(key) 160 | } 161 | 162 | public override func objectForKey(key: String, completion: cacheCompletion) { 163 | completion(self.memoryCache.objectForKey(key)) 164 | } 165 | 166 | public override func setObject(object: AnyObject, key: String) { 167 | self.memoryCache.setObject(object, forKey: key) 168 | } 169 | 170 | public override func removeObject(key: String) { 171 | self.memoryCache.removeObjectForKey(key) 172 | } 173 | 174 | public override func removeAllObject() { 175 | self.memoryCache.removeAllObjects() 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/Models/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // HackerSwifter 4 | // 5 | // Created by Tosin Afolabi on 17/07/2014. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc public class Comment: NSObject, NSCoding { 12 | 13 | public var type: CommentFilter? 14 | public var text: String? 15 | public var username: String? 16 | public var depth: Int = 0 17 | public var commentId: String? 18 | public var parentId: String? 19 | public var prettyTime: String? 20 | public var links: [NSURL]? 21 | public var replyURLString: String? 22 | public var upvoteURLAddition: String? 23 | public var downvoteURLAddition: String? 24 | 25 | public enum CommentFilter: String { 26 | case Default = "default" 27 | case Ask = "ask" 28 | case Jobs = "jobs" 29 | } 30 | 31 | internal enum serialization: String { 32 | case text = "text" 33 | case username = "username" 34 | case depth = "depth" 35 | case commentId = "commentId" 36 | case parentId = "parentId" 37 | case prettyTime = "prettyTime" 38 | case links = "links" 39 | case replyURLString = "replyURLString" 40 | case upvoteURLAddition = "upvoteURLAddition" 41 | case downvoteURLAddition = "downvoteURLAddition" 42 | 43 | static let values = [text, username, depth, commentId, parentId, prettyTime, links, 44 | replyURLString, upvoteURLAddition, downvoteURLAddition] 45 | } 46 | 47 | public override init(){ 48 | super.init() 49 | } 50 | 51 | public init(html: String, type: Post.PostFilter) { 52 | super.init() 53 | self.parseHTML(html, withType: type) 54 | } 55 | 56 | public required init?(coder aDecoder: NSCoder) { 57 | super.init() 58 | 59 | for key in serialization.values { 60 | setValue(aDecoder.decodeObjectForKey(key.rawValue), forKey: key.rawValue) 61 | } 62 | } 63 | 64 | public func encodeWithCoder(aCoder: NSCoder) { 65 | for key in serialization.values { 66 | if let value: AnyObject = self.valueForKey(key.rawValue) { 67 | aCoder.encodeObject(value, forKey: key.rawValue) 68 | } 69 | } 70 | } 71 | } 72 | 73 | //MARK: Equatable implementation 74 | public func ==(larg: Comment, rarg: Comment) -> Bool { 75 | return larg.commentId == rarg.commentId 76 | } 77 | 78 | //MARK: Network 79 | public extension Comment { 80 | 81 | typealias Response = (comments: [Comment]!, error: Fetcher.ResponseError!, local: Bool) -> Void 82 | 83 | public class func fetch(forPost post: Post, completion: Response) { 84 | let ressource = "item?id=" + post.postId! 85 | Fetcher.Fetch(ressource, 86 | parsing: {(html) in 87 | var type = post.type 88 | if type == nil { 89 | type = Post.PostFilter.Default 90 | } 91 | 92 | if let realHtml = html { 93 | let comments = self.parseCollectionHTML(realHtml, withType: type!) 94 | return comments 95 | } 96 | else { 97 | return nil 98 | } 99 | }, 100 | completion: {(object, error, local) in 101 | if let realObject: AnyObject = object { 102 | completion(comments: realObject as! [Comment], error: error, local: local) 103 | } 104 | else { 105 | completion(comments: nil, error: error, local: local) 106 | } 107 | }) 108 | } 109 | } 110 | 111 | 112 | 113 | //MARK: HTML 114 | internal extension Comment { 115 | 116 | internal class func parseCollectionHTML(html: String, withType type: Post.PostFilter) -> [Comment] { 117 | var components = html.componentsSeparatedByString(" 0) { 120 | if (type == Post.PostFilter.Ask) { 121 | let scanner = NSScanner(string: components[0]) 122 | let comment = Comment() 123 | comment.type = CommentFilter.Ask 124 | comment.commentId = scanner.scanTag("") 125 | comment.username = scanner.scanTag("by ") 126 | comment.prettyTime = scanner.scanTag(" ", endTag: "ago") + "ago" 127 | comment.text = String.stringByRemovingHTMLEntities(scanner.scanTag("", endTag: "")) 128 | comment.depth = 0 129 | comments.append(comment) 130 | } 131 | 132 | else if (type == Post.PostFilter.Jobs) { 133 | let scanner = NSScanner(string: components[0]) 134 | let comment = Comment() 135 | comment.depth = 0 136 | comment.text = String.stringByRemovingHTMLEntities(scanner.scanTag("", endTag: "")) 137 | comment.type = CommentFilter.Jobs 138 | comments.append(comment) 139 | } 140 | 141 | var index = 0 142 | 143 | for component in components { 144 | if index != 0 && index != components.count - 1 { 145 | comments.append(Comment(html: component, type: type)) 146 | } 147 | index++ 148 | } 149 | } 150 | return comments 151 | } 152 | 153 | internal func parseHTML(html: String, withType type: Post.PostFilter) { 154 | let scanner = NSScanner(string: html) 155 | 156 | let level = scanner.scanTag("height=\"1\" width=\"", endTag: ">") 157 | if let unwrappedLevel = Int(level.substringToIndex(level.startIndex.advancedBy(level.characters.count - 1))) { 158 | self.depth = unwrappedLevel / 40 159 | } else { 160 | self.depth = 0 161 | } 162 | 163 | let username = scanner.scanTag("") 164 | self.username = username.utf16.count > 0 ? username : "[deleted]" 165 | self.commentId = scanner.scanTag("") 166 | self.prettyTime = scanner.scanTag(">", endTag: "") 167 | 168 | if (html.rangeOfString("[deleted]")?.startIndex != nil) { 169 | self.text = "[deleted]" 170 | } else { 171 | let textTemp = scanner.scanTag("") as String 172 | if (textTemp.characters.count>0) { 173 | self.text = String.stringByRemovingHTMLEntities(textTemp.substringFromIndex(textTemp.startIndex.advancedBy(10))) 174 | } 175 | else { 176 | self.text = "" 177 | } 178 | } 179 | 180 | //LOL, it whould always work, as I strip a Hex color, which is always the same length 181 | 182 | self.replyURLString = scanner.scanTag("reply") 183 | self.type = CommentFilter.Default 184 | } 185 | } 186 | 187 | -------------------------------------------------------------------------------- /Hacker Swifter/Hacker Swifter/Models/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // Hacker Swifter 4 | // 5 | // Created by Thomas Ricouard on 11/07/14. 6 | // Copyright (c) 2014 Thomas Ricouard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(Post) public class Post: NSObject, NSCoding { 12 | 13 | public var id: Int? 14 | public var title: String? 15 | public var username: String? 16 | public var url: NSURL? 17 | public var domain: String? { 18 | get { 19 | if let realUrl = self.url { 20 | if let host = realUrl.host { 21 | if (host.hasPrefix("www")) { 22 | return host.substringFromIndex(host.startIndex.advancedBy(4)) 23 | } 24 | return host 25 | } 26 | } 27 | return "" 28 | } 29 | } 30 | public var points: Int = 0 31 | public var commentsCount: Int = 0 32 | public var postId: String? 33 | public var prettyTime: String? 34 | public var upvoteURL: String? 35 | public var type: PostFilter? 36 | public var kids: [Int]? 37 | public var score: Int? 38 | public var time: Int? 39 | public var dead: Bool = false 40 | 41 | public enum PostFilter: String { 42 | case Top = "" 43 | case Default = "default" 44 | case Ask = "ask" 45 | case New = "newest" 46 | case Jobs = "jobs" 47 | case Best = "best" 48 | case Show = "show" 49 | } 50 | 51 | internal enum serialization: String { 52 | case title = "title" 53 | case username = "username" 54 | case url = "url" 55 | case points = "points" 56 | case commentsCount = "commentsCount" 57 | case postId = "postId" 58 | case prettyTime = "prettyTime" 59 | case upvoteURL = "upvoteURL" 60 | 61 | static let values = [title, username, url, points, commentsCount, postId, prettyTime, upvoteURL] 62 | } 63 | 64 | internal enum JSONField: String { 65 | case id = "id" 66 | case by = "by" 67 | case descendants = "descendants" 68 | case kids = "kids" 69 | case score = "score" 70 | case time = "time" 71 | case title = "title" 72 | case type = "type" 73 | case url = "url" 74 | case dead = "dead" 75 | } 76 | 77 | public override init(){ 78 | super.init() 79 | } 80 | 81 | public init(html: String) { 82 | super.init() 83 | self.parseHTML(html) 84 | } 85 | 86 | public init(json: NSDictionary) { 87 | super.init() 88 | self.parseJSON(json) 89 | } 90 | 91 | required public init?(coder aDecoder: NSCoder) { 92 | super.init() 93 | 94 | for key in serialization.values { 95 | setValue(aDecoder.decodeObjectForKey(key.rawValue), forKey: key.rawValue) 96 | } 97 | } 98 | 99 | public func encodeWithCoder(aCoder: NSCoder) { 100 | for key in serialization.values { 101 | if let value: AnyObject = self.valueForKey(key.rawValue) { 102 | aCoder.encodeObject(value, forKey: key.rawValue) 103 | } 104 | } 105 | } 106 | 107 | private func encode(object: AnyObject!, key: String, coder: NSCoder) { 108 | if let _: AnyObject = object { 109 | coder.encodeObject(object, forKey: key) 110 | } 111 | } 112 | } 113 | 114 | //MARK: Equatable implementation 115 | public func ==(larg: Post, rarg: Post) -> Bool { 116 | return larg.postId == rarg.postId 117 | } 118 | 119 | //MARK: Network 120 | public extension Post { 121 | 122 | public typealias Response = (posts: [Post]!, error: Fetcher.ResponseError!, local: Bool) -> Void 123 | public typealias ResponsePost = (post: Post!, error: Fetcher.ResponseError!, local: Bool) -> Void 124 | public typealias ResponsePosts = (post: [Int]!, error: Fetcher.ResponseError!, local: Bool) -> Void 125 | 126 | public class func fetch(filter: PostFilter, page: Int, completion: Response) { 127 | Fetcher.Fetch(filter.rawValue + "?p=\(page)", 128 | parsing: {(html) in 129 | if let realHtml = html { 130 | let posts = self.parseCollectionHTML(realHtml) 131 | return posts 132 | } else { 133 | return nil 134 | } 135 | }, 136 | completion: {(object, error, local) in 137 | if let realObject: AnyObject = object { 138 | completion(posts: realObject as! [Post], error: error, local: local) 139 | } 140 | else { 141 | completion(posts: nil, error: error, local: local) 142 | } 143 | }) 144 | } 145 | 146 | public class func fetch(filter: PostFilter, completion: Response) { 147 | fetch(filter, page: 1, completion: completion) 148 | } 149 | 150 | public class func fetch(user: String, page: Int, lastPostId:String?, completion: Response) { 151 | var additionalParameters = "" 152 | if let lastPostIdInt = Int(lastPostId ?? "") { 153 | additionalParameters = "&next=\(lastPostIdInt-1)" 154 | } 155 | Fetcher.Fetch("submitted?id=" + user + additionalParameters, 156 | parsing: {(html) in 157 | if let realHtml = html { 158 | let posts = self.parseCollectionHTML(realHtml) 159 | return posts 160 | } else { 161 | return nil 162 | } 163 | }, 164 | completion: {(object, error, local) in 165 | if let realObject: AnyObject = object { 166 | completion(posts: realObject as! [Post], error: error, local: local) 167 | } 168 | else { 169 | completion(posts: nil, error: error, local: local) 170 | } 171 | }) 172 | } 173 | 174 | public class func fetch(user: String, completion: Response) { 175 | fetch(user, page: 1, lastPostId:nil, completion: completion) 176 | } 177 | 178 | public class func fetchPost(completion: ResponsePosts) { 179 | Fetcher.FetchJSON(.Top, ressource: nil, parsing: { (json) -> AnyObject! in 180 | if let _ = json as? [Int] { 181 | return json 182 | } 183 | return nil 184 | }) { (object, error, local) -> Void in 185 | completion(post: object as? [Int] , error: error, local: local) 186 | } 187 | } 188 | public class func fetchPost(post: Int, completion: ResponsePost) { 189 | Fetcher.FetchJSON(.Post, ressource: String(post), parsing: { (json) -> AnyObject! in 190 | if let dic = json as? NSDictionary { 191 | return Post(json: dic) 192 | } 193 | return nil 194 | }) 195 | { (object, error, local) -> Void in 196 | completion(post: object as! Post, error: error, local: local) 197 | } 198 | } 199 | } 200 | 201 | //MARK: JSON 202 | 203 | internal extension Post { 204 | internal func parseJSON(json: NSDictionary) { 205 | self.id = json[JSONField.id.rawValue] as? Int 206 | if let kids = json[JSONField.kids.rawValue] as? [Int] { 207 | self.kids = kids 208 | } 209 | self.title = json[JSONField.title.rawValue] as? String 210 | self.score = json[JSONField.score.rawValue] as? Int 211 | self.username = json[JSONField.by.rawValue] as? String 212 | self.time = json[JSONField.time.rawValue] as? Int 213 | self.url = NSURL(string: (json[JSONField.url.rawValue] as? String)!) 214 | if let commentsCount = json[JSONField.descendants.rawValue] as? Int { 215 | self.commentsCount = commentsCount 216 | } 217 | if let _ = json[JSONField.dead.rawValue] as? Bool { 218 | self.dead = true 219 | } 220 | } 221 | } 222 | 223 | //MARK: HTML 224 | internal extension Post { 225 | 226 | internal class func parseCollectionHTML(html: String) -> [Post] { 227 | let components = html.componentsSeparatedByString("") 228 | var posts: [Post] = [] 229 | if (components.count > 0) { 230 | var index = 0 231 | for component in components { 232 | if index != 0 { 233 | posts.append(Post(html: component)) 234 | } 235 | index++ 236 | } 237 | } 238 | return posts 239 | } 240 | 241 | internal func parseHTML(html: String) { 242 | let scanner = NSScanner(string: html) 243 | 244 | if (html.rangeOfString(" [dead] ", endTag: "") 248 | 249 | var temp: NSString = scanner.scanTag("") 250 | let range = temp.rangeOfString(">") 251 | if (range.location != NSNotFound) { 252 | let tmpPoint: Int? = Int(temp.substringFromIndex(range.location + 1) 253 | .stringByReplacingOccurrencesOfString(" points", withString: "", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)) 254 | if let points = tmpPoint { 255 | self.points = points 256 | } 257 | else { 258 | self.points = 0 259 | } 260 | } 261 | else { 262 | self.points = 0 263 | } 264 | self.username = scanner.scanTag("") 269 | self.prettyTime = scanner.scanTag(">", endTag: "") 270 | 271 | temp = scanner.scanTag("\">", endTag: "") 272 | if (temp == "discuss") { 273 | self.commentsCount = 0 274 | } 275 | else { 276 | self.commentsCount = temp.integerValue 277 | } 278 | if (self.username == nil && self.commentsCount == 0 && self.postId == nil) { 279 | self.type = PostFilter.Jobs 280 | self.username = "Jobs" 281 | } 282 | else if (self.url?.absoluteString.localizedCaseInsensitiveCompare("http") == nil) { 283 | self.type = PostFilter.Ask 284 | if let realURL = self.url { 285 | let url = realURL.absoluteString 286 | self.url = NSURL(string: "https://news.ycombinator.com/" + url) 287 | } 288 | } 289 | else { 290 | self.type = PostFilter.Default 291 | } 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /Hacker Swifter/HackerSwifter.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4E7CCB5E197747EA006917BA /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7CCB5D197747EA006917BA /* Comment.swift */; }; 11 | 4EE778231978264500978F28 /* CommentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77820197823E000978F28 /* CommentTests.swift */; }; 12 | 9F6872F61979536C00716FE0 /* HTMLString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6872F51979536C00716FE0 /* HTMLString.swift */; }; 13 | 9F6AEEAE19768F86007D95BD /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6AEEAD19768F86007D95BD /* Cache.swift */; }; 14 | 9F6AEEB01976AC2C007D95BD /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6AEEAF1976AC2C007D95BD /* CacheTests.swift */; }; 15 | 9FD4A19C196F3007003608F6 /* Hacker Swifter.h in Headers */ = {isa = PBXBuildFile; fileRef = 9FD4A19B196F3007003608F6 /* Hacker Swifter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16 | 9FD4A1A2196F3008003608F6 /* HackerSwifter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FD4A196196F3007003608F6 /* HackerSwifter.framework */; }; 17 | 9FD4A1A9196F3008003608F6 /* Hacker_SwifterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1A8196F3008003608F6 /* Hacker_SwifterTests.swift */; }; 18 | 9FD4A1B419704397003608F6 /* HTMLScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1B319704397003608F6 /* HTMLScanner.swift */; }; 19 | 9FD4A1B7197046AF003608F6 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1B6197046AF003608F6 /* Post.swift */; }; 20 | 9FD4A1B9197048DB003608F6 /* Fetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1B8197048DB003608F6 /* Fetcher.swift */; }; 21 | 9FD4A1BC1970604B003608F6 /* PostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1BB1970604B003608F6 /* PostTests.swift */; }; 22 | 9FD4A1BE19706066003608F6 /* FetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1BD19706066003608F6 /* FetcherTests.swift */; }; 23 | 9FE8C8131C2A9B1D001D91EF /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7CCB5D197747EA006917BA /* Comment.swift */; }; 24 | 9FE8C8141C2A9B1D001D91EF /* HTMLScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1B319704397003608F6 /* HTMLScanner.swift */; }; 25 | 9FE8C8151C2A9B1D001D91EF /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1B6197046AF003608F6 /* Post.swift */; }; 26 | 9FE8C8161C2A9B1D001D91EF /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6AEEAD19768F86007D95BD /* Cache.swift */; }; 27 | 9FE8C8171C2A9B1D001D91EF /* Fetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD4A1B8197048DB003608F6 /* Fetcher.swift */; }; 28 | 9FE8C8181C2A9B1D001D91EF /* HTMLString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6872F51979536C00716FE0 /* HTMLString.swift */; }; 29 | 9FE8C81B1C2A9B1D001D91EF /* Hacker Swifter.h in Headers */ = {isa = PBXBuildFile; fileRef = 9FD4A19B196F3007003608F6 /* Hacker Swifter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 9FD4A1A3196F3008003608F6 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 9FD4A18D196F3007003608F6 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 9FD4A195196F3007003608F6; 38 | remoteInfo = "Hacker Swifter"; 39 | }; 40 | 9FD4A1BF19706222003608F6 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 9FD4A18D196F3007003608F6 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 9FD4A195196F3007003608F6; 45 | remoteInfo = "Hacker Swifter"; 46 | }; 47 | 9FD4A1C119706222003608F6 /* PBXContainerItemProxy */ = { 48 | isa = PBXContainerItemProxy; 49 | containerPortal = 9FD4A18D196F3007003608F6 /* Project object */; 50 | proxyType = 1; 51 | remoteGlobalIDString = 9FD4A195196F3007003608F6; 52 | remoteInfo = "Hacker Swifter"; 53 | }; 54 | 9FD4A1C319706224003608F6 /* PBXContainerItemProxy */ = { 55 | isa = PBXContainerItemProxy; 56 | containerPortal = 9FD4A18D196F3007003608F6 /* Project object */; 57 | proxyType = 1; 58 | remoteGlobalIDString = 9FD4A195196F3007003608F6; 59 | remoteInfo = "Hacker Swifter"; 60 | }; 61 | /* End PBXContainerItemProxy section */ 62 | 63 | /* Begin PBXFileReference section */ 64 | 4E7CCB5D197747EA006917BA /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Comment.swift; path = Models/Comment.swift; sourceTree = ""; }; 65 | 4EE77820197823E000978F28 /* CommentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentTests.swift; sourceTree = ""; }; 66 | 9F6872F51979536C00716FE0 /* HTMLString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTMLString.swift; path = Extensions/HTMLString.swift; sourceTree = ""; }; 67 | 9F6AEEAD19768F86007D95BD /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Cache.swift; path = HTTP/Cache.swift; sourceTree = ""; }; 68 | 9F6AEEAF1976AC2C007D95BD /* CacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTests.swift; sourceTree = ""; }; 69 | 9FD4A196196F3007003608F6 /* HackerSwifter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HackerSwifter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 9FD4A19A196F3007003608F6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | 9FD4A19B196F3007003608F6 /* Hacker Swifter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Hacker Swifter.h"; sourceTree = ""; }; 72 | 9FD4A1A1196F3008003608F6 /* HackerSwifterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HackerSwifterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 73 | 9FD4A1A7196F3008003608F6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 9FD4A1A8196F3008003608F6 /* Hacker_SwifterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hacker_SwifterTests.swift; sourceTree = ""; }; 75 | 9FD4A1B319704397003608F6 /* HTMLScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTMLScanner.swift; path = Extensions/HTMLScanner.swift; sourceTree = ""; }; 76 | 9FD4A1B6197046AF003608F6 /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Post.swift; path = Models/Post.swift; sourceTree = ""; wrapsLines = 0; }; 77 | 9FD4A1B8197048DB003608F6 /* Fetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Fetcher.swift; path = HTTP/Fetcher.swift; sourceTree = ""; }; 78 | 9FD4A1BB1970604B003608F6 /* PostTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostTests.swift; sourceTree = ""; }; 79 | 9FD4A1BD19706066003608F6 /* FetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetcherTests.swift; sourceTree = ""; }; 80 | 9FE8C8201C2A9B1D001D91EF /* HackerSwifterWatch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HackerSwifterWatch.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 9FE8C8211C2A9B1D001D91EF /* HackerSwifter copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "HackerSwifter copy-Info.plist"; path = "/Users/Dimillian/Documents/DEV/Dev_iphone/Swift/HackerSwifter/Hacker Swifter/HackerSwifter copy-Info.plist"; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 9FD4A192196F3007003608F6 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | 9FD4A19E196F3008003608F6 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | 9FD4A1A2196F3008003608F6 /* HackerSwifter.framework in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | 9FE8C8191C2A9B1D001D91EF /* Frameworks */ = { 101 | isa = PBXFrameworksBuildPhase; 102 | buildActionMask = 2147483647; 103 | files = ( 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | /* End PBXFrameworksBuildPhase section */ 108 | 109 | /* Begin PBXGroup section */ 110 | 9FD4A18C196F3007003608F6 = { 111 | isa = PBXGroup; 112 | children = ( 113 | 9FD4A198196F3007003608F6 /* Hacker Swifter */, 114 | 9FD4A1A5196F3008003608F6 /* Hacker SwifterTests */, 115 | 9FD4A197196F3007003608F6 /* Products */, 116 | 9FE8C8211C2A9B1D001D91EF /* HackerSwifter copy-Info.plist */, 117 | ); 118 | sourceTree = ""; 119 | }; 120 | 9FD4A197196F3007003608F6 /* Products */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 9FD4A196196F3007003608F6 /* HackerSwifter.framework */, 124 | 9FD4A1A1196F3008003608F6 /* HackerSwifterTests.xctest */, 125 | 9FE8C8201C2A9B1D001D91EF /* HackerSwifterWatch.framework */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | 9FD4A198196F3007003608F6 /* Hacker Swifter */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 9FD4A1BA197048E1003608F6 /* HTTP */, 134 | 9FD4A1B519704698003608F6 /* Models */, 135 | 9FD4A1B21970436B003608F6 /* Extension */, 136 | 9FD4A19B196F3007003608F6 /* Hacker Swifter.h */, 137 | 9FD4A199196F3007003608F6 /* Supporting Files */, 138 | ); 139 | path = "Hacker Swifter"; 140 | sourceTree = ""; 141 | }; 142 | 9FD4A199196F3007003608F6 /* Supporting Files */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 9FD4A19A196F3007003608F6 /* Info.plist */, 146 | ); 147 | name = "Supporting Files"; 148 | sourceTree = ""; 149 | }; 150 | 9FD4A1A5196F3008003608F6 /* Hacker SwifterTests */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 9FD4A1A8196F3008003608F6 /* Hacker_SwifterTests.swift */, 154 | 9FD4A1BB1970604B003608F6 /* PostTests.swift */, 155 | 4EE77820197823E000978F28 /* CommentTests.swift */, 156 | 9FD4A1BD19706066003608F6 /* FetcherTests.swift */, 157 | 9F6AEEAF1976AC2C007D95BD /* CacheTests.swift */, 158 | 9FD4A1A6196F3008003608F6 /* Supporting Files */, 159 | ); 160 | path = "Hacker SwifterTests"; 161 | sourceTree = ""; 162 | }; 163 | 9FD4A1A6196F3008003608F6 /* Supporting Files */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | 9FD4A1A7196F3008003608F6 /* Info.plist */, 167 | ); 168 | name = "Supporting Files"; 169 | sourceTree = ""; 170 | }; 171 | 9FD4A1B21970436B003608F6 /* Extension */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 9FD4A1B319704397003608F6 /* HTMLScanner.swift */, 175 | 9F6872F51979536C00716FE0 /* HTMLString.swift */, 176 | ); 177 | name = Extension; 178 | sourceTree = ""; 179 | }; 180 | 9FD4A1B519704698003608F6 /* Models */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 9FD4A1B6197046AF003608F6 /* Post.swift */, 184 | 4E7CCB5D197747EA006917BA /* Comment.swift */, 185 | ); 186 | name = Models; 187 | sourceTree = ""; 188 | }; 189 | 9FD4A1BA197048E1003608F6 /* HTTP */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 9FD4A1B8197048DB003608F6 /* Fetcher.swift */, 193 | 9F6AEEAD19768F86007D95BD /* Cache.swift */, 194 | ); 195 | name = HTTP; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXGroup section */ 199 | 200 | /* Begin PBXHeadersBuildPhase section */ 201 | 9FD4A193196F3007003608F6 /* Headers */ = { 202 | isa = PBXHeadersBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | 9FD4A19C196F3007003608F6 /* Hacker Swifter.h in Headers */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | 9FE8C81A1C2A9B1D001D91EF /* Headers */ = { 210 | isa = PBXHeadersBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | 9FE8C81B1C2A9B1D001D91EF /* Hacker Swifter.h in Headers */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXHeadersBuildPhase section */ 218 | 219 | /* Begin PBXNativeTarget section */ 220 | 9FD4A195196F3007003608F6 /* HackerSwifter */ = { 221 | isa = PBXNativeTarget; 222 | buildConfigurationList = 9FD4A1AC196F3008003608F6 /* Build configuration list for PBXNativeTarget "HackerSwifter" */; 223 | buildPhases = ( 224 | 9FD4A191196F3007003608F6 /* Sources */, 225 | 9FD4A192196F3007003608F6 /* Frameworks */, 226 | 9FD4A193196F3007003608F6 /* Headers */, 227 | 9FD4A194196F3007003608F6 /* Resources */, 228 | ); 229 | buildRules = ( 230 | ); 231 | dependencies = ( 232 | ); 233 | name = HackerSwifter; 234 | productName = "Hacker Swifter"; 235 | productReference = 9FD4A196196F3007003608F6 /* HackerSwifter.framework */; 236 | productType = "com.apple.product-type.framework"; 237 | }; 238 | 9FD4A1A0196F3008003608F6 /* HackerSwifterTests */ = { 239 | isa = PBXNativeTarget; 240 | buildConfigurationList = 9FD4A1AF196F3008003608F6 /* Build configuration list for PBXNativeTarget "HackerSwifterTests" */; 241 | buildPhases = ( 242 | 9FD4A19D196F3008003608F6 /* Sources */, 243 | 9FD4A19E196F3008003608F6 /* Frameworks */, 244 | 9FD4A19F196F3008003608F6 /* Resources */, 245 | ); 246 | buildRules = ( 247 | ); 248 | dependencies = ( 249 | 9FD4A1A4196F3008003608F6 /* PBXTargetDependency */, 250 | 9FD4A1C019706222003608F6 /* PBXTargetDependency */, 251 | 9FD4A1C219706222003608F6 /* PBXTargetDependency */, 252 | 9FD4A1C419706224003608F6 /* PBXTargetDependency */, 253 | ); 254 | name = HackerSwifterTests; 255 | productName = "Hacker SwifterTests"; 256 | productReference = 9FD4A1A1196F3008003608F6 /* HackerSwifterTests.xctest */; 257 | productType = "com.apple.product-type.bundle.unit-test"; 258 | }; 259 | 9FE8C8111C2A9B1D001D91EF /* HackerSwifterWatch */ = { 260 | isa = PBXNativeTarget; 261 | buildConfigurationList = 9FE8C81D1C2A9B1D001D91EF /* Build configuration list for PBXNativeTarget "HackerSwifterWatch" */; 262 | buildPhases = ( 263 | 9FE8C8121C2A9B1D001D91EF /* Sources */, 264 | 9FE8C8191C2A9B1D001D91EF /* Frameworks */, 265 | 9FE8C81A1C2A9B1D001D91EF /* Headers */, 266 | 9FE8C81C1C2A9B1D001D91EF /* Resources */, 267 | ); 268 | buildRules = ( 269 | ); 270 | dependencies = ( 271 | ); 272 | name = HackerSwifterWatch; 273 | productName = "Hacker Swifter"; 274 | productReference = 9FE8C8201C2A9B1D001D91EF /* HackerSwifterWatch.framework */; 275 | productType = "com.apple.product-type.framework"; 276 | }; 277 | /* End PBXNativeTarget section */ 278 | 279 | /* Begin PBXProject section */ 280 | 9FD4A18D196F3007003608F6 /* Project object */ = { 281 | isa = PBXProject; 282 | attributes = { 283 | LastSwiftMigration = 0720; 284 | LastSwiftUpdateCheck = 0720; 285 | LastUpgradeCheck = 0720; 286 | ORGANIZATIONNAME = "Thomas Ricouard"; 287 | TargetAttributes = { 288 | 9FD4A195196F3007003608F6 = { 289 | CreatedOnToolsVersion = 6.0; 290 | DevelopmentTeam = Z6P74P6T99; 291 | }; 292 | 9FD4A1A0196F3008003608F6 = { 293 | CreatedOnToolsVersion = 6.0; 294 | TestTargetID = 9FD4A195196F3007003608F6; 295 | }; 296 | }; 297 | }; 298 | buildConfigurationList = 9FD4A190196F3007003608F6 /* Build configuration list for PBXProject "HackerSwifter" */; 299 | compatibilityVersion = "Xcode 3.2"; 300 | developmentRegion = English; 301 | hasScannedForEncodings = 0; 302 | knownRegions = ( 303 | en, 304 | ); 305 | mainGroup = 9FD4A18C196F3007003608F6; 306 | productRefGroup = 9FD4A197196F3007003608F6 /* Products */; 307 | projectDirPath = ""; 308 | projectRoot = ""; 309 | targets = ( 310 | 9FD4A195196F3007003608F6 /* HackerSwifter */, 311 | 9FD4A1A0196F3008003608F6 /* HackerSwifterTests */, 312 | 9FE8C8111C2A9B1D001D91EF /* HackerSwifterWatch */, 313 | ); 314 | }; 315 | /* End PBXProject section */ 316 | 317 | /* Begin PBXResourcesBuildPhase section */ 318 | 9FD4A194196F3007003608F6 /* Resources */ = { 319 | isa = PBXResourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | }; 325 | 9FD4A19F196F3008003608F6 /* Resources */ = { 326 | isa = PBXResourcesBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | 9FE8C81C1C2A9B1D001D91EF /* Resources */ = { 333 | isa = PBXResourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | ); 337 | runOnlyForDeploymentPostprocessing = 0; 338 | }; 339 | /* End PBXResourcesBuildPhase section */ 340 | 341 | /* Begin PBXSourcesBuildPhase section */ 342 | 9FD4A191196F3007003608F6 /* Sources */ = { 343 | isa = PBXSourcesBuildPhase; 344 | buildActionMask = 2147483647; 345 | files = ( 346 | 4E7CCB5E197747EA006917BA /* Comment.swift in Sources */, 347 | 9FD4A1B419704397003608F6 /* HTMLScanner.swift in Sources */, 348 | 9FD4A1B7197046AF003608F6 /* Post.swift in Sources */, 349 | 9F6AEEAE19768F86007D95BD /* Cache.swift in Sources */, 350 | 9FD4A1B9197048DB003608F6 /* Fetcher.swift in Sources */, 351 | 9F6872F61979536C00716FE0 /* HTMLString.swift in Sources */, 352 | ); 353 | runOnlyForDeploymentPostprocessing = 0; 354 | }; 355 | 9FD4A19D196F3008003608F6 /* Sources */ = { 356 | isa = PBXSourcesBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | 9FD4A1A9196F3008003608F6 /* Hacker_SwifterTests.swift in Sources */, 360 | 4EE778231978264500978F28 /* CommentTests.swift in Sources */, 361 | 9FD4A1BC1970604B003608F6 /* PostTests.swift in Sources */, 362 | 9F6AEEB01976AC2C007D95BD /* CacheTests.swift in Sources */, 363 | 9FD4A1BE19706066003608F6 /* FetcherTests.swift in Sources */, 364 | ); 365 | runOnlyForDeploymentPostprocessing = 0; 366 | }; 367 | 9FE8C8121C2A9B1D001D91EF /* Sources */ = { 368 | isa = PBXSourcesBuildPhase; 369 | buildActionMask = 2147483647; 370 | files = ( 371 | 9FE8C8131C2A9B1D001D91EF /* Comment.swift in Sources */, 372 | 9FE8C8141C2A9B1D001D91EF /* HTMLScanner.swift in Sources */, 373 | 9FE8C8151C2A9B1D001D91EF /* Post.swift in Sources */, 374 | 9FE8C8161C2A9B1D001D91EF /* Cache.swift in Sources */, 375 | 9FE8C8171C2A9B1D001D91EF /* Fetcher.swift in Sources */, 376 | 9FE8C8181C2A9B1D001D91EF /* HTMLString.swift in Sources */, 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | /* End PBXSourcesBuildPhase section */ 381 | 382 | /* Begin PBXTargetDependency section */ 383 | 9FD4A1A4196F3008003608F6 /* PBXTargetDependency */ = { 384 | isa = PBXTargetDependency; 385 | target = 9FD4A195196F3007003608F6 /* HackerSwifter */; 386 | targetProxy = 9FD4A1A3196F3008003608F6 /* PBXContainerItemProxy */; 387 | }; 388 | 9FD4A1C019706222003608F6 /* PBXTargetDependency */ = { 389 | isa = PBXTargetDependency; 390 | target = 9FD4A195196F3007003608F6 /* HackerSwifter */; 391 | targetProxy = 9FD4A1BF19706222003608F6 /* PBXContainerItemProxy */; 392 | }; 393 | 9FD4A1C219706222003608F6 /* PBXTargetDependency */ = { 394 | isa = PBXTargetDependency; 395 | target = 9FD4A195196F3007003608F6 /* HackerSwifter */; 396 | targetProxy = 9FD4A1C119706222003608F6 /* PBXContainerItemProxy */; 397 | }; 398 | 9FD4A1C419706224003608F6 /* PBXTargetDependency */ = { 399 | isa = PBXTargetDependency; 400 | target = 9FD4A195196F3007003608F6 /* HackerSwifter */; 401 | targetProxy = 9FD4A1C319706224003608F6 /* PBXContainerItemProxy */; 402 | }; 403 | /* End PBXTargetDependency section */ 404 | 405 | /* Begin XCBuildConfiguration section */ 406 | 9FD4A1AA196F3008003608F6 /* Debug */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_SEARCH_USER_PATHS = NO; 410 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 411 | CLANG_CXX_LIBRARY = "libc++"; 412 | CLANG_ENABLE_MODULES = YES; 413 | CLANG_ENABLE_OBJC_ARC = YES; 414 | CLANG_WARN_BOOL_CONVERSION = YES; 415 | CLANG_WARN_CONSTANT_CONVERSION = YES; 416 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 417 | CLANG_WARN_EMPTY_BODY = YES; 418 | CLANG_WARN_ENUM_CONVERSION = YES; 419 | CLANG_WARN_INT_CONVERSION = YES; 420 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 424 | COPY_PHASE_STRIP = NO; 425 | CURRENT_PROJECT_VERSION = 1; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | ENABLE_TESTABILITY = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu99; 429 | GCC_DYNAMIC_NO_PIC = NO; 430 | GCC_OPTIMIZATION_LEVEL = 0; 431 | GCC_PREPROCESSOR_DEFINITIONS = ( 432 | "DEBUG=1", 433 | "$(inherited)", 434 | ); 435 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 443 | MTL_ENABLE_DEBUG_INFO = YES; 444 | ONLY_ACTIVE_ARCH = YES; 445 | SDKROOT = iphoneos; 446 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 447 | TARGETED_DEVICE_FAMILY = "1,2"; 448 | VERSIONING_SYSTEM = "apple-generic"; 449 | VERSION_INFO_PREFIX = ""; 450 | }; 451 | name = Debug; 452 | }; 453 | 9FD4A1AB196F3008003608F6 /* Release */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_SEARCH_USER_PATHS = NO; 457 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 458 | CLANG_CXX_LIBRARY = "libc++"; 459 | CLANG_ENABLE_MODULES = YES; 460 | CLANG_ENABLE_OBJC_ARC = YES; 461 | CLANG_WARN_BOOL_CONVERSION = YES; 462 | CLANG_WARN_CONSTANT_CONVERSION = YES; 463 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 464 | CLANG_WARN_EMPTY_BODY = YES; 465 | CLANG_WARN_ENUM_CONVERSION = YES; 466 | CLANG_WARN_INT_CONVERSION = YES; 467 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 468 | CLANG_WARN_UNREACHABLE_CODE = YES; 469 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 470 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 471 | COPY_PHASE_STRIP = YES; 472 | CURRENT_PROJECT_VERSION = 1; 473 | ENABLE_NS_ASSERTIONS = NO; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | GCC_C_LANGUAGE_STANDARD = gnu99; 476 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 477 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 478 | GCC_WARN_UNDECLARED_SELECTOR = YES; 479 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 480 | GCC_WARN_UNUSED_FUNCTION = YES; 481 | GCC_WARN_UNUSED_VARIABLE = YES; 482 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 483 | MTL_ENABLE_DEBUG_INFO = NO; 484 | SDKROOT = iphoneos; 485 | TARGETED_DEVICE_FAMILY = "1,2"; 486 | VALIDATE_PRODUCT = YES; 487 | VERSIONING_SYSTEM = "apple-generic"; 488 | VERSION_INFO_PREFIX = ""; 489 | }; 490 | name = Release; 491 | }; 492 | 9FD4A1AD196F3008003608F6 /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | APPLICATION_EXTENSION_API_ONLY = YES; 496 | CLANG_ENABLE_MODULES = YES; 497 | CODE_SIGN_IDENTITY = "iPhone Developer"; 498 | DEFINES_MODULE = YES; 499 | DYLIB_COMPATIBILITY_VERSION = 1; 500 | DYLIB_CURRENT_VERSION = 1; 501 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 502 | INFOPLIST_FILE = "Hacker Swifter/Info.plist"; 503 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 505 | PRODUCT_BUNDLE_IDENTIFIER = "com.hacker.swifter.${PRODUCT_NAME:rfc1034identifier}"; 506 | PRODUCT_NAME = HackerSwifter; 507 | SKIP_INSTALL = YES; 508 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 509 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 510 | }; 511 | name = Debug; 512 | }; 513 | 9FD4A1AE196F3008003608F6 /* Release */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | APPLICATION_EXTENSION_API_ONLY = YES; 517 | CLANG_ENABLE_MODULES = YES; 518 | CODE_SIGN_IDENTITY = "iPhone Developer"; 519 | DEFINES_MODULE = YES; 520 | DYLIB_COMPATIBILITY_VERSION = 1; 521 | DYLIB_CURRENT_VERSION = 1; 522 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 523 | GCC_OPTIMIZATION_LEVEL = 0; 524 | INFOPLIST_FILE = "Hacker Swifter/Info.plist"; 525 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 527 | PRODUCT_BUNDLE_IDENTIFIER = "com.hacker.swifter.${PRODUCT_NAME:rfc1034identifier}"; 528 | PRODUCT_NAME = HackerSwifter; 529 | SKIP_INSTALL = YES; 530 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 531 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 532 | }; 533 | name = Release; 534 | }; 535 | 9FD4A1B0196F3008003608F6 /* Debug */ = { 536 | isa = XCBuildConfiguration; 537 | buildSettings = { 538 | FRAMEWORK_SEARCH_PATHS = ( 539 | "$(SDKROOT)/Developer/Library/Frameworks", 540 | "$(inherited)", 541 | ); 542 | GCC_PREPROCESSOR_DEFINITIONS = ( 543 | "DEBUG=1", 544 | "$(inherited)", 545 | ); 546 | INFOPLIST_FILE = "Hacker SwifterTests/Info.plist"; 547 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 548 | PRODUCT_BUNDLE_IDENTIFIER = "com.hacker.swifter.${PRODUCT_NAME:rfc1034identifier}"; 549 | PRODUCT_NAME = HackerSwifterTests; 550 | }; 551 | name = Debug; 552 | }; 553 | 9FD4A1B1196F3008003608F6 /* Release */ = { 554 | isa = XCBuildConfiguration; 555 | buildSettings = { 556 | FRAMEWORK_SEARCH_PATHS = ( 557 | "$(SDKROOT)/Developer/Library/Frameworks", 558 | "$(inherited)", 559 | ); 560 | INFOPLIST_FILE = "Hacker SwifterTests/Info.plist"; 561 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 562 | PRODUCT_BUNDLE_IDENTIFIER = "com.hacker.swifter.${PRODUCT_NAME:rfc1034identifier}"; 563 | PRODUCT_NAME = HackerSwifterTests; 564 | }; 565 | name = Release; 566 | }; 567 | 9FE8C81E1C2A9B1D001D91EF /* Debug */ = { 568 | isa = XCBuildConfiguration; 569 | buildSettings = { 570 | APPLICATION_EXTENSION_API_ONLY = YES; 571 | CLANG_ENABLE_MODULES = YES; 572 | CODE_SIGN_IDENTITY = "iPhone Developer"; 573 | DEFINES_MODULE = YES; 574 | DYLIB_COMPATIBILITY_VERSION = 1; 575 | DYLIB_CURRENT_VERSION = 1; 576 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 577 | INFOPLIST_FILE = "HackerSwifter copy-Info.plist"; 578 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 579 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 580 | PRODUCT_BUNDLE_IDENTIFIER = "com.hacker.swifter.${PRODUCT_NAME:rfc1034identifier}"; 581 | PRODUCT_NAME = "$(TARGET_NAME)"; 582 | SKIP_INSTALL = YES; 583 | SUPPORTED_PLATFORMS = "watchsimulator watchos"; 584 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 585 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 586 | }; 587 | name = Debug; 588 | }; 589 | 9FE8C81F1C2A9B1D001D91EF /* Release */ = { 590 | isa = XCBuildConfiguration; 591 | buildSettings = { 592 | APPLICATION_EXTENSION_API_ONLY = YES; 593 | CLANG_ENABLE_MODULES = YES; 594 | CODE_SIGN_IDENTITY = "iPhone Developer"; 595 | DEFINES_MODULE = YES; 596 | DYLIB_COMPATIBILITY_VERSION = 1; 597 | DYLIB_CURRENT_VERSION = 1; 598 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 599 | GCC_OPTIMIZATION_LEVEL = 0; 600 | INFOPLIST_FILE = "HackerSwifter copy-Info.plist"; 601 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 602 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 603 | PRODUCT_BUNDLE_IDENTIFIER = "com.hacker.swifter.${PRODUCT_NAME:rfc1034identifier}"; 604 | PRODUCT_NAME = "$(TARGET_NAME)"; 605 | SKIP_INSTALL = YES; 606 | SUPPORTED_PLATFORMS = "watchsimulator watchos"; 607 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 608 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 609 | }; 610 | name = Release; 611 | }; 612 | /* End XCBuildConfiguration section */ 613 | 614 | /* Begin XCConfigurationList section */ 615 | 9FD4A190196F3007003608F6 /* Build configuration list for PBXProject "HackerSwifter" */ = { 616 | isa = XCConfigurationList; 617 | buildConfigurations = ( 618 | 9FD4A1AA196F3008003608F6 /* Debug */, 619 | 9FD4A1AB196F3008003608F6 /* Release */, 620 | ); 621 | defaultConfigurationIsVisible = 0; 622 | defaultConfigurationName = Release; 623 | }; 624 | 9FD4A1AC196F3008003608F6 /* Build configuration list for PBXNativeTarget "HackerSwifter" */ = { 625 | isa = XCConfigurationList; 626 | buildConfigurations = ( 627 | 9FD4A1AD196F3008003608F6 /* Debug */, 628 | 9FD4A1AE196F3008003608F6 /* Release */, 629 | ); 630 | defaultConfigurationIsVisible = 0; 631 | defaultConfigurationName = Release; 632 | }; 633 | 9FD4A1AF196F3008003608F6 /* Build configuration list for PBXNativeTarget "HackerSwifterTests" */ = { 634 | isa = XCConfigurationList; 635 | buildConfigurations = ( 636 | 9FD4A1B0196F3008003608F6 /* Debug */, 637 | 9FD4A1B1196F3008003608F6 /* Release */, 638 | ); 639 | defaultConfigurationIsVisible = 0; 640 | defaultConfigurationName = Release; 641 | }; 642 | 9FE8C81D1C2A9B1D001D91EF /* Build configuration list for PBXNativeTarget "HackerSwifterWatch" */ = { 643 | isa = XCConfigurationList; 644 | buildConfigurations = ( 645 | 9FE8C81E1C2A9B1D001D91EF /* Debug */, 646 | 9FE8C81F1C2A9B1D001D91EF /* Release */, 647 | ); 648 | defaultConfigurationIsVisible = 0; 649 | defaultConfigurationName = Release; 650 | }; 651 | /* End XCConfigurationList section */ 652 | }; 653 | rootObject = 9FD4A18D196F3007003608F6 /* Project object */; 654 | } 655 | --------------------------------------------------------------------------------