├── .DS_Store ├── .gitignore ├── Dependencies ├── .DS_Store └── CommonCrypto.swift │ ├── .gitignore │ ├── .slather.yml │ ├── .travis.yml │ ├── CONTRIBUTING.md │ ├── CommonCryptoSwift.podspec │ ├── CommonCryptoSwift.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── CommonCryptoSwift-iOS.xcscheme │ ├── CommonCryptoSwift │ ├── Info-Mac.plist │ └── Info-iOS.plist │ ├── CommonCryptoSwiftTests │ ├── Info-Mac.plist │ ├── Info-iOS.plist │ └── Tests.swift │ ├── Example │ └── CommonCryptoSwiftDemo │ │ ├── CommonCryptoSwiftDemo.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── CommonCryptoSwiftDemo.xcscheme │ │ ├── CommonCryptoSwiftDemo.xcworkspace │ │ └── contents.xcworkspacedata │ │ ├── CommonCryptoSwiftDemo │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── Resources │ │ │ └── Assets.xcassets │ │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── Sources │ │ │ ├── AppDelegate.swift │ │ │ └── ViewController.swift │ │ ├── Podfile │ │ └── Podfile.lock │ ├── LICENSE.md │ ├── Playground-Mac.playground │ ├── Contents.swift │ └── contents.xcplayground │ ├── Playground-iOS.playground │ ├── Contents.swift │ └── contents.xcplayground │ ├── README.md │ ├── Screenshots │ └── Banner.png │ └── Sources │ ├── CCommonCrypto │ └── module.modulemap │ ├── Crypto.swift │ ├── HMAC.swift │ ├── Hash.swift │ └── NSData+Extensions.swift ├── LICENSE ├── README.md ├── Sodes ├── .DS_Store ├── Sodes.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── Cast.xcscmblueprint │ │ │ └── Sodes.xcscmblueprint │ └── xcshareddata │ │ └── xcschemes │ │ ├── SodesAudio.xcscheme │ │ ├── SodesFoundation.xcscheme │ │ └── SwiftableFileHandle.xcscheme ├── SodesAudio │ ├── .DS_Store │ ├── AVContentInfoRequest.swift │ ├── ByteRange.swift │ ├── Data.swift │ ├── DataRequestLoader.swift │ ├── Errors.swift │ ├── Info.plist │ ├── MiscExtensions.swift │ ├── NSObject.swift │ ├── PlaybackController.swift │ ├── PlaybackSource.swift │ ├── Requests.swift │ ├── ResourceLoaderDelegate.swift │ ├── ResourceLoaderSubrequest.swift │ ├── SODSwiftableFileHandle.swift │ ├── ScratchFileInfo.swift │ ├── ScratchFileInfoSerialization.swift │ ├── URL.swift │ ├── URLRequest.swift │ └── URLResponse.swift ├── SodesAudioTests │ ├── ByteRangeSerializationTests.swift │ ├── ByteRangeTests.swift │ ├── DataTests.swift │ ├── Info.plist │ ├── ResourceLoaderSubrequestTests.swift │ ├── URLRequestTests.swift │ └── URLResponseTests.swift ├── SodesExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── EpisodeDuration.swift │ ├── Info.plist │ └── ViewController.swift ├── SodesFoundation │ ├── AsyncBlockOperation.swift │ ├── DelegatedHTTPOperation.swift │ ├── DownloadOperation.swift │ ├── Error.swift │ ├── FileManager.swift │ ├── GCD.swift │ ├── HTTPOperation.swift │ ├── Info.plist │ ├── NSDateFormatter.swift │ ├── SodesLog.swift │ ├── Throttle.swift │ └── WorkOperation.swift ├── SodesFoundationTests │ ├── DateFormattingTests.swift │ ├── DelegatedHTTPOperationTests.swift │ └── Info.plist └── SwiftableFileHandle │ ├── Info.plist │ ├── SODSwiftableFileHandle.h │ ├── SODSwiftableFileHandle.m │ └── SwiftableFileHandle.h └── screenshot.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /Dependencies/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Dependencies/.DS_Store -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | ._* 7 | .Spotlight-V100 8 | .Trashes 9 | 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 | 29 | # CocoaPods 30 | Pods 31 | 32 | # Carthage 33 | Carthage 34 | 35 | # SPM 36 | .build/ 37 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/.slather.yml: -------------------------------------------------------------------------------- 1 | ci_service: travis_ci 2 | coverage_service: coveralls 3 | xcodeproj: CommonCryptoSwift.xcodeproj 4 | source_directory: Sources 5 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode7.3 2 | language: objective-c 3 | 4 | before_install: 5 | - brew update 6 | - if brew outdated | grep -qx carthage; then brew upgrade carthage; fi 7 | - travis_wait 35 carthage bootstrap --platform iOS,Mac 8 | 9 | script: 10 | - xcodebuild clean build -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-iOS -sdk iphonesimulator 11 | - xcodebuild test -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-iOS -sdk iphonesimulator 12 | - xcodebuild clean build -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-Mac -sdk macosx 13 | - xcodebuild test -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-Mac -sdk macosx 14 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | GitHub Issues is for reporting bugs, discussing features and general feedback in **CommonCryptoSwift**. Be sure to check our [documentation](http://cocoadocs.org/docsets/CommonCryptoSwift), [FAQ](https://github.com/onmyway133/CommonCryptoSwift/wiki/FAQ) and [past issues](https://github.com/onmyway133/CommonCryptoSwift/issues?state=closed) before opening any new issues. 2 | 3 | If you are posting about a crash in your application, a stack trace is helpful, but additional context, in the form of code and explanation, is necessary to be of any use. 4 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CommonCryptoSwift" 3 | s.summary = "CommonCrypto in Swift" 4 | s.version = "0.1.0" 5 | s.homepage = "https://github.com/onmyway133/CommonCrypto.swift" 6 | s.license = 'MIT' 7 | s.author = { "Khoa Pham" => "onmyway133@gmail.com" } 8 | s.source = { 9 | :git => "https://github.com/onmyway133/CommonCrypto.swift.git", 10 | :tag => s.version.to_s 11 | } 12 | s.social_media_url = 'https://twitter.com/onmyway133' 13 | 14 | s.ios.deployment_target = '8.0' 15 | s.osx.deployment_target = '10.9' 16 | s.tvos.deployment_target = '9.2' 17 | 18 | s.requires_arc = true 19 | s.source_files = 'Sources/**/*.swift' 20 | s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2', 'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/CommonCryptoSwift/Sources/CCommonCrypto' } 21 | s.preserve_paths = 'Sources/CCommonCrypto/module.modulemap' 22 | 23 | end 24 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwift.xcodeproj/xcshareddata/xcschemes/CommonCryptoSwift-iOS.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 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwift/Info-Mac.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 | NSHumanReadableCopyright 24 | Copyright © 2016 Hyper Interaktiv AS. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwift/Info-iOS.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 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwiftTests/Info-Mac.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 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwiftTests/Info-iOS.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 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/CommonCryptoSwiftTests/Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.swift 3 | // CommonCryptoSwift 4 | // 5 | // Created by Khoa Pham on 07/05/16. 6 | // Copyright © 2016 Khoa Pham. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CommonCryptoSwift 11 | 12 | class Tests: XCTestCase { 13 | 14 | func testHash() { 15 | let string = "https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg" 16 | 17 | XCTAssertEqual(Hash.MD2(string), "e4a410bf8a43197e67a01d717bbf3557") 18 | XCTAssertEqual(Hash.MD4(string), "b263c99c66fd5703b894c020b224f71e") 19 | XCTAssertEqual(Hash.MD5(string), "0dfb10e8d2ae771b3b3ed4544139644e") 20 | 21 | XCTAssertEqual(Hash.SHA1(string), "cd99bfa31d38bb53850196b8f1daca692231def5") 22 | XCTAssertEqual(Hash.SHA224(string), "b0caf544bc75698e1efe4d55acf014ddb6ab3b5b14d312e1a5fe42d3") 23 | XCTAssertEqual(Hash.SHA256(string), "cb051d58a60b9581ff4c7ba63da07f9170f61bfbebab4a39898432ec970c3754") 24 | XCTAssertEqual(Hash.SHA384(string), "ee999f4e722bdab4534a4c3b9d33a13e37ab4bf227348d7218ef32f7483ee09f05f5f15e69e09d7d53dec46d4df16275") 25 | XCTAssertEqual(Hash.SHA512(string), "13416866022ace1b3392c0d7a7fb1e311612d3e30e9bafa9045ed9cdcd14ea4c31fb3b18d716c44ccecf8c18be5c063a0883bbc60f964ce002890fb628cf35c7") 26 | } 27 | 28 | func testHMAC() { 29 | let string = "https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg" 30 | let key = "google" 31 | 32 | XCTAssertEqual(HMAC.MD5(string, key: key), "419337f8da2e81cdf12dcb9b8e4cd76c") 33 | XCTAssertEqual(HMAC.SHA1(string, key: key), "5f4474c8872d73c1490241ab015f6c672c6dcdc8") 34 | XCTAssertEqual(HMAC.SHA224(string, key: key), "82a903faa4f93c528f490c699c9bfef0c0ef8a3498dd677cfab0a71e") 35 | XCTAssertEqual(HMAC.SHA256(string, key: key), "a8e314bf001c5ea640c374582f5a27d9d29a39cb1b1c729748bad5365dfb0632") 36 | XCTAssertEqual(HMAC.SHA384(string, key: key), "afa2026322d2843e564a9d88e2a03a5a4f75581386217052d952bdda7a7af6a48ba4e0b927229cdec46cdf7e5ffa9ee3") 37 | XCTAssertEqual(HMAC.SHA512(string, key: key), "d75af2a8f76dd22b3e7a52f84593e3c9d56065a675f5e96ecf3e1354d6dba05f357418fbfbd3af0491e69f548bb4200600d139d601741c6c9eb37952e2e38d7a") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 97F29A739B5DAF048446AB43 /* Pods_CommonCryptoSwiftDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C8E2B9C49E327C0C4BC5B2C /* Pods_CommonCryptoSwiftDemo.framework */; }; 11 | D5C7F74E1C3BC9CE008CDDBA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */; }; 12 | D5C7F75B1C3BCA1E008CDDBA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */; }; 13 | D5C7F75C1C3BCA1E008CDDBA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */; }; 14 | D5C7F75D1C3BCA1E008CDDBA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 0CA6DC52BD90B876037F7603 /* Pods-CommonCryptoSwiftDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CommonCryptoSwiftDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo.release.xcconfig"; sourceTree = ""; }; 19 | 2C8E2B9C49E327C0C4BC5B2C /* Pods_CommonCryptoSwiftDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CommonCryptoSwiftDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 5D55BF7BF84BEDD023F53C68 /* Pods-CommonCryptoSwiftDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CommonCryptoSwiftDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo.debug.xcconfig"; sourceTree = ""; }; 21 | D5C7F7401C3BC9CE008CDDBA /* CommonCryptoSwiftDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CommonCryptoSwiftDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | D5C7F74D1C3BC9CE008CDDBA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 23 | D5C7F74F1C3BC9CE008CDDBA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | D5C7F73D1C3BC9CE008CDDBA /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 97F29A739B5DAF048446AB43 /* Pods_CommonCryptoSwiftDemo.framework in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 2E7CE5304D233567E667E08C /* Pods */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | 5D55BF7BF84BEDD023F53C68 /* Pods-CommonCryptoSwiftDemo.debug.xcconfig */, 45 | 0CA6DC52BD90B876037F7603 /* Pods-CommonCryptoSwiftDemo.release.xcconfig */, 46 | ); 47 | name = Pods; 48 | sourceTree = ""; 49 | }; 50 | 42CC7077ACF00910DFC948C7 /* Frameworks */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 2C8E2B9C49E327C0C4BC5B2C /* Pods_CommonCryptoSwiftDemo.framework */, 54 | ); 55 | name = Frameworks; 56 | sourceTree = ""; 57 | }; 58 | D5C7F7371C3BC9CE008CDDBA = { 59 | isa = PBXGroup; 60 | children = ( 61 | D5C7F7421C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */, 62 | D5C7F7411C3BC9CE008CDDBA /* Products */, 63 | 2E7CE5304D233567E667E08C /* Pods */, 64 | 42CC7077ACF00910DFC948C7 /* Frameworks */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | D5C7F7411C3BC9CE008CDDBA /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | D5C7F7401C3BC9CE008CDDBA /* CommonCryptoSwiftDemo.app */, 72 | ); 73 | name = Products; 74 | sourceTree = ""; 75 | }; 76 | D5C7F7421C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | D5C7F7561C3BCA1E008CDDBA /* Resources */, 80 | D5C7F7581C3BCA1E008CDDBA /* Sources */, 81 | D5C7F7551C3BC9EA008CDDBA /* Supporting Files */, 82 | ); 83 | path = CommonCryptoSwiftDemo; 84 | sourceTree = ""; 85 | }; 86 | D5C7F7551C3BC9EA008CDDBA /* Supporting Files */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */, 90 | D5C7F74F1C3BC9CE008CDDBA /* Info.plist */, 91 | ); 92 | name = "Supporting Files"; 93 | sourceTree = ""; 94 | }; 95 | D5C7F7561C3BCA1E008CDDBA /* Resources */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */, 99 | ); 100 | path = Resources; 101 | sourceTree = ""; 102 | }; 103 | D5C7F7581C3BCA1E008CDDBA /* Sources */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */, 107 | D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */, 108 | ); 109 | path = Sources; 110 | sourceTree = ""; 111 | }; 112 | /* End PBXGroup section */ 113 | 114 | /* Begin PBXNativeTarget section */ 115 | D5C7F73F1C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */ = { 116 | isa = PBXNativeTarget; 117 | buildConfigurationList = D5C7F7521C3BC9CE008CDDBA /* Build configuration list for PBXNativeTarget "CommonCryptoSwiftDemo" */; 118 | buildPhases = ( 119 | C7D3FFB9DCD3A4C6AA2DA385 /* 📦 Check Pods Manifest.lock */, 120 | D5C7F73C1C3BC9CE008CDDBA /* Sources */, 121 | D5C7F73D1C3BC9CE008CDDBA /* Frameworks */, 122 | D5C7F73E1C3BC9CE008CDDBA /* Resources */, 123 | 68E6206C43FDCED318C2C79A /* 📦 Embed Pods Frameworks */, 124 | 2B4CDA8F16A1BF04F2805683 /* 📦 Copy Pods Resources */, 125 | ); 126 | buildRules = ( 127 | ); 128 | dependencies = ( 129 | ); 130 | name = CommonCryptoSwiftDemo; 131 | productName = CommonCryptoSwiftDemo; 132 | productReference = D5C7F7401C3BC9CE008CDDBA /* CommonCryptoSwiftDemo.app */; 133 | productType = "com.apple.product-type.application"; 134 | }; 135 | /* End PBXNativeTarget section */ 136 | 137 | /* Begin PBXProject section */ 138 | D5C7F7381C3BC9CE008CDDBA /* Project object */ = { 139 | isa = PBXProject; 140 | attributes = { 141 | LastSwiftUpdateCheck = 0720; 142 | LastUpgradeCheck = 0720; 143 | ORGANIZATIONNAME = "Hyper Interaktiv AS"; 144 | TargetAttributes = { 145 | D5C7F73F1C3BC9CE008CDDBA = { 146 | CreatedOnToolsVersion = 7.2; 147 | }; 148 | }; 149 | }; 150 | buildConfigurationList = D5C7F73B1C3BC9CE008CDDBA /* Build configuration list for PBXProject "CommonCryptoSwiftDemo" */; 151 | compatibilityVersion = "Xcode 3.2"; 152 | developmentRegion = English; 153 | hasScannedForEncodings = 0; 154 | knownRegions = ( 155 | en, 156 | Base, 157 | ); 158 | mainGroup = D5C7F7371C3BC9CE008CDDBA; 159 | productRefGroup = D5C7F7411C3BC9CE008CDDBA /* Products */; 160 | projectDirPath = ""; 161 | projectRoot = ""; 162 | targets = ( 163 | D5C7F73F1C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */, 164 | ); 165 | }; 166 | /* End PBXProject section */ 167 | 168 | /* Begin PBXResourcesBuildPhase section */ 169 | D5C7F73E1C3BC9CE008CDDBA /* Resources */ = { 170 | isa = PBXResourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | D5C7F75B1C3BCA1E008CDDBA /* Assets.xcassets in Resources */, 174 | D5C7F74E1C3BC9CE008CDDBA /* LaunchScreen.storyboard in Resources */, 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | }; 178 | /* End PBXResourcesBuildPhase section */ 179 | 180 | /* Begin PBXShellScriptBuildPhase section */ 181 | 2B4CDA8F16A1BF04F2805683 /* 📦 Copy Pods Resources */ = { 182 | isa = PBXShellScriptBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | ); 186 | inputPaths = ( 187 | ); 188 | name = "📦 Copy Pods Resources"; 189 | outputPaths = ( 190 | ); 191 | runOnlyForDeploymentPostprocessing = 0; 192 | shellPath = /bin/sh; 193 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo-resources.sh\"\n"; 194 | showEnvVarsInLog = 0; 195 | }; 196 | 68E6206C43FDCED318C2C79A /* 📦 Embed Pods Frameworks */ = { 197 | isa = PBXShellScriptBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | ); 201 | inputPaths = ( 202 | ); 203 | name = "📦 Embed Pods Frameworks"; 204 | outputPaths = ( 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | shellPath = /bin/sh; 208 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo-frameworks.sh\"\n"; 209 | showEnvVarsInLog = 0; 210 | }; 211 | C7D3FFB9DCD3A4C6AA2DA385 /* 📦 Check Pods Manifest.lock */ = { 212 | isa = PBXShellScriptBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | ); 216 | inputPaths = ( 217 | ); 218 | name = "📦 Check Pods Manifest.lock"; 219 | outputPaths = ( 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | shellPath = /bin/sh; 223 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 224 | showEnvVarsInLog = 0; 225 | }; 226 | /* End PBXShellScriptBuildPhase section */ 227 | 228 | /* Begin PBXSourcesBuildPhase section */ 229 | D5C7F73C1C3BC9CE008CDDBA /* Sources */ = { 230 | isa = PBXSourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | D5C7F75D1C3BCA1E008CDDBA /* ViewController.swift in Sources */, 234 | D5C7F75C1C3BCA1E008CDDBA /* AppDelegate.swift in Sources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | /* End PBXSourcesBuildPhase section */ 239 | 240 | /* Begin PBXVariantGroup section */ 241 | D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */ = { 242 | isa = PBXVariantGroup; 243 | children = ( 244 | D5C7F74D1C3BC9CE008CDDBA /* Base */, 245 | ); 246 | name = LaunchScreen.storyboard; 247 | sourceTree = ""; 248 | }; 249 | /* End PBXVariantGroup section */ 250 | 251 | /* Begin XCBuildConfiguration section */ 252 | D5C7F7501C3BC9CE008CDDBA /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | ALWAYS_SEARCH_USER_PATHS = NO; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_WARN_BOOL_CONVERSION = YES; 261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 263 | CLANG_WARN_EMPTY_BODY = YES; 264 | CLANG_WARN_ENUM_CONVERSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 270 | COPY_PHASE_STRIP = NO; 271 | DEBUG_INFORMATION_FORMAT = dwarf; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | ENABLE_TESTABILITY = YES; 274 | GCC_C_LANGUAGE_STANDARD = gnu99; 275 | GCC_DYNAMIC_NO_PIC = NO; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_OPTIMIZATION_LEVEL = 0; 278 | GCC_PREPROCESSOR_DEFINITIONS = ( 279 | "DEBUG=1", 280 | "$(inherited)", 281 | ); 282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 284 | GCC_WARN_UNDECLARED_SELECTOR = YES; 285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 286 | GCC_WARN_UNUSED_FUNCTION = YES; 287 | GCC_WARN_UNUSED_VARIABLE = YES; 288 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 289 | MTL_ENABLE_DEBUG_INFO = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = iphoneos; 292 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 293 | }; 294 | name = Debug; 295 | }; 296 | D5C7F7511C3BC9CE008CDDBA /* Release */ = { 297 | isa = XCBuildConfiguration; 298 | buildSettings = { 299 | ALWAYS_SEARCH_USER_PATHS = NO; 300 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 301 | CLANG_CXX_LIBRARY = "libc++"; 302 | CLANG_ENABLE_MODULES = YES; 303 | CLANG_ENABLE_OBJC_ARC = YES; 304 | CLANG_WARN_BOOL_CONVERSION = YES; 305 | CLANG_WARN_CONSTANT_CONVERSION = YES; 306 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 307 | CLANG_WARN_EMPTY_BODY = YES; 308 | CLANG_WARN_ENUM_CONVERSION = YES; 309 | CLANG_WARN_INT_CONVERSION = YES; 310 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 311 | CLANG_WARN_UNREACHABLE_CODE = YES; 312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 313 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu99; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 327 | MTL_ENABLE_DEBUG_INFO = NO; 328 | SDKROOT = iphoneos; 329 | VALIDATE_PRODUCT = YES; 330 | }; 331 | name = Release; 332 | }; 333 | D5C7F7531C3BC9CE008CDDBA /* Debug */ = { 334 | isa = XCBuildConfiguration; 335 | baseConfigurationReference = 5D55BF7BF84BEDD023F53C68 /* Pods-CommonCryptoSwiftDemo.debug.xcconfig */; 336 | buildSettings = { 337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 338 | INFOPLIST_FILE = CommonCryptoSwiftDemo/Info.plist; 339 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 340 | PRODUCT_BUNDLE_IDENTIFIER = com.fantageek.CommonCryptoSwift.CommonCryptoSwiftDemo; 341 | PRODUCT_NAME = "$(TARGET_NAME)"; 342 | }; 343 | name = Debug; 344 | }; 345 | D5C7F7541C3BC9CE008CDDBA /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | baseConfigurationReference = 0CA6DC52BD90B876037F7603 /* Pods-CommonCryptoSwiftDemo.release.xcconfig */; 348 | buildSettings = { 349 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 350 | INFOPLIST_FILE = CommonCryptoSwiftDemo/Info.plist; 351 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 352 | PRODUCT_BUNDLE_IDENTIFIER = com.fantageek.CommonCryptoSwift.CommonCryptoSwiftDemo; 353 | PRODUCT_NAME = "$(TARGET_NAME)"; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | D5C7F73B1C3BC9CE008CDDBA /* Build configuration list for PBXProject "CommonCryptoSwiftDemo" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | D5C7F7501C3BC9CE008CDDBA /* Debug */, 364 | D5C7F7511C3BC9CE008CDDBA /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | D5C7F7521C3BC9CE008CDDBA /* Build configuration list for PBXNativeTarget "CommonCryptoSwiftDemo" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | D5C7F7531C3BC9CE008CDDBA /* Debug */, 373 | D5C7F7541C3BC9CE008CDDBA /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | }; 380 | rootObject = D5C7F7381C3BC9CE008CDDBA /* Project object */; 381 | } 382 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcodeproj/xcshareddata/xcschemes/CommonCryptoSwiftDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/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 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CommonCryptoSwift 3 | 4 | @UIApplicationMain 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | 7 | var window: UIWindow? 8 | 9 | lazy var navigationController: UINavigationController = { [unowned self] in 10 | let controller = UINavigationController(rootViewController: self.viewController) 11 | return controller 12 | }() 13 | 14 | lazy var viewController: ViewController = { 15 | let controller = ViewController() 16 | return controller 17 | }() 18 | 19 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 20 | window = UIWindow(frame: UIScreen.mainScreen().bounds) 21 | window?.rootViewController = navigationController 22 | window?.makeKeyAndVisible() 23 | 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CommonCryptoSwift 3 | 4 | class ViewController: UIViewController { 5 | 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | assert(Hash.MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg") == "0dfb10e8d2ae771b3b3ed4544139644e") 10 | print(Hash.MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg")) 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '8.0' 4 | 5 | target 'CommonCryptoSwiftDemo' do 6 | pod 'CommonCryptoSwift', :git => 'https://github.com/onmyway133/CommonCrypto.swift' 7 | end 8 | 9 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CommonCryptoSwift (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - CommonCryptoSwift (from `https://github.com/onmyway133/CommonCrypto.swift`) 6 | 7 | EXTERNAL SOURCES: 8 | CommonCryptoSwift: 9 | :git: https://github.com/onmyway133/CommonCrypto.swift 10 | 11 | CHECKOUT OPTIONS: 12 | CommonCryptoSwift: 13 | :commit: c4d758ec195bb38284cb2149dc6dee21ad5dd1e6 14 | :git: https://github.com/onmyway133/CommonCrypto.swift 15 | 16 | SPEC CHECKSUMS: 17 | CommonCryptoSwift: 3248a2412fac3fced7ed51c62b3a9d1932e21823 18 | 19 | PODFILE CHECKSUM: a2e5ffbdf0fc0edb82758b54102d3f6af53280f9 20 | 21 | COCOAPODS: 1.0.0.beta.8 22 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the **MIT** license 2 | 3 | > Copyright (c) 2015 Khoa Pham 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Playground-Mac.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | // CommonCryptoSwift Mac Playground 2 | 3 | import Cocoa 4 | import CommonCryptoSwift 5 | 6 | var str = "Hello, playground" 7 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Playground-Mac.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Playground-iOS.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | // CommonCryptoSwift iOS Playground 2 | 3 | import UIKit 4 | import CommonCryptoSwift 5 | 6 | var str = "Hello, playground" 7 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Playground-iOS.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/README.md: -------------------------------------------------------------------------------- 1 | # CommonCrypto.swift 2 | CommonCrypto in Swift 3 | 4 | [![CI Status](http://img.shields.io/travis/onmyway133/CommonCryptoSwift.svg?style=flat)](https://travis-ci.org/onmyway133/CommonCryptoSwift) 5 | [![Version](https://img.shields.io/cocoapods/v/CommonCryptoSwift.svg?style=flat)](http://cocoadocs.org/docsets/CommonCryptoSwift) 6 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![License](https://img.shields.io/cocoapods/l/CommonCryptoSwift.svg?style=flat)](http://cocoadocs.org/docsets/CommonCryptoSwift) 8 | [![Platform](https://img.shields.io/cocoapods/p/CommonCryptoSwift.svg?style=flat)](http://cocoadocs.org/docsets/CommonCryptoSwift) 9 | 10 | ![](Screenshots/Banner.png) 11 | 12 | ## Features 13 | 14 | - Work on NSData, String 15 | - Message Digest, SHA, HMAC 16 | - Hash Algorithm: MD2, MD4, MD5, SHA1, SHA256, SHA224, SHA384, SHA512 17 | 18 | ```swift 19 | Hash.MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg") // 0dfb10e8d2ae771b3b3ed4544139644e 20 | Hash.SHA246("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg") // cb051d58a60b9581ff4c7ba63da07f9170f61bfbebab4a39898432ec970c3754 21 | HMAC.SHA1("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg", key: "google") // 5f4474c8872d73c1490241ab015f6c672c6dcdc8 22 | ``` 23 | 24 | ## Installation 25 | 26 | **CommonCryptoSwift** is available through [CocoaPods](http://cocoapods.org). To install 27 | it, simply add the following line to your Podfile: 28 | 29 | ```ruby 30 | pod 'CommonCryptoSwift', git: 'https://github.com/onmyway133/CommonCrypto.swift' 31 | ``` 32 | 33 | **CommonCryptoSwift** is also available through [Carthage](https://github.com/Carthage/Carthage). 34 | To install just write into your Cartfile: 35 | 36 | ```ruby 37 | github "onmyway133/CommonCrypto.swift" 38 | ``` 39 | 40 | ## Author 41 | 42 | Khoa Pham, onmyway133@gmail.com 43 | 44 | ## Contributing 45 | 46 | We would love you to contribute to **CommonCryptoSwift**, check the [CONTRIBUTING](https://github.com/onmyway133/CommonCryptoSwift/blob/master/CONTRIBUTING.md) file for more info. 47 | 48 | ## License 49 | 50 | **CommonCryptoSwift** is available under the MIT license. See the [LICENSE](https://github.com/onmyway133/CommonCryptoSwift/blob/master/LICENSE.md) file for more info. 51 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Screenshots/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Dependencies/CommonCrypto.swift/Screenshots/Banner.png -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Sources/CCommonCrypto/module.modulemap: -------------------------------------------------------------------------------- 1 | module CCommonCrypto { 2 | header "/usr/include/CommonCrypto/CommonCrypto.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Sources/Crypto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Crypto.swift 3 | // CommonCryptoSwift 4 | // 5 | // Created by Khoa Pham on 08/05/16. 6 | // Copyright © 2016 Fantageek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CCommonCrypto 11 | 12 | typealias DigestMethod = (_ data: UnsafeRawPointer, _ len: CC_LONG, _ md: UnsafeMutablePointer) -> UnsafeMutablePointer! 13 | 14 | struct Crypto { 15 | let length: Int32 16 | let method: DigestMethod 17 | let HMACAlgorithm: CCHmacAlgorithm? 18 | 19 | static let MD2 = Crypto(length: CC_MD2_DIGEST_LENGTH, method: CC_MD2, 20 | HMACAlgorithm: nil) 21 | static let MD4 = Crypto(length: CC_MD4_DIGEST_LENGTH, method: CC_MD4, 22 | HMACAlgorithm: nil) 23 | static let MD5 = Crypto(length: CC_MD5_DIGEST_LENGTH, method: CC_MD5, 24 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgMD5)) 25 | static let SHA1 = Crypto(length: CC_SHA1_DIGEST_LENGTH, method: CC_SHA1, 26 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA1)) 27 | static let SHA224 = Crypto(length: CC_SHA224_DIGEST_LENGTH, method: CC_SHA224, 28 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA224)) 29 | static let SHA256 = Crypto(length: CC_SHA256_DIGEST_LENGTH, method: CC_SHA256, 30 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA256)) 31 | static let SHA384 = Crypto(length: CC_SHA384_DIGEST_LENGTH, method: CC_SHA384, 32 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA384)) 33 | static let SHA512 = Crypto(length: CC_SHA512_DIGEST_LENGTH, method: CC_SHA512, 34 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA512)) 35 | } 36 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Sources/HMAC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HMAC.swift 3 | // CommonCryptoSwift 4 | // 5 | // Created by Khoa Pham on 08/05/16. 6 | // Copyright © 2016 Fantageek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CCommonCrypto 11 | 12 | public struct HMAC { 13 | 14 | // MARK: - NSData 15 | 16 | public static func MD5(data: NSData, key: NSData) -> NSData? { 17 | return HMAC.generate(data: data, key: key, crypto: .MD5) 18 | } 19 | 20 | public static func SHA1(data: NSData, key: NSData) -> NSData? { 21 | return HMAC.generate(data: data, key: key, crypto: .SHA1) 22 | } 23 | 24 | public static func SHA224(data: NSData, key: NSData) -> NSData? { 25 | return HMAC.generate(data: data, key: key, crypto: .SHA224) 26 | } 27 | 28 | public static func SHA256(data: NSData, key: NSData) -> NSData? { 29 | return HMAC.generate(data: data, key: key, crypto: .SHA256) 30 | } 31 | 32 | public static func SHA384(data: NSData, key: NSData) -> NSData? { 33 | return HMAC.generate(data: data, key: key, crypto: .SHA384) 34 | } 35 | 36 | public static func SHA512(data: NSData, key: NSData) -> NSData? { 37 | return HMAC.generate(data: data, key: key, crypto: .SHA512) 38 | } 39 | 40 | static func generate(data: NSData, key: NSData, crypto: Crypto) -> NSData? { 41 | guard let HMACAlgorithm = crypto.HMACAlgorithm else { return nil } 42 | 43 | // Can also use UnsafeMutablePointer.alloc(Int(crypto.length)) 44 | var buffer = Array(repeating: 0, count: Int(crypto.length)) 45 | CCHmac(HMACAlgorithm, key.bytes, key.length, data.bytes, data.length, &buffer) 46 | 47 | return NSData(bytes: buffer, length: Int(crypto.length)) 48 | } 49 | 50 | // MARK: - String 51 | 52 | public static func MD5(string: String, key: String) -> String? { 53 | return HMAC.generate(string: string, key: key, crypto: .MD5) 54 | } 55 | 56 | public static func SHA1(string: String, key: String) -> String? { 57 | return HMAC.generate(string: string, key: key, crypto: .SHA1) 58 | } 59 | 60 | public static func SHA224(string: String, key: String) -> String? { 61 | return HMAC.generate(string: string, key: key, crypto: .SHA224) 62 | } 63 | 64 | public static func SHA256(string: String, key: String) -> String? { 65 | return HMAC.generate(string: string, key: key, crypto: .SHA256) 66 | } 67 | 68 | public static func SHA384(string: String, key: String) -> String? { 69 | return HMAC.generate(string: string, key: key, crypto: .SHA384) 70 | } 71 | 72 | public static func SHA512(string: String, key: String) -> String? { 73 | return HMAC.generate(string: string, key: key, crypto: .SHA512) 74 | } 75 | 76 | static func generate(string: String, key: String, crypto: Crypto) -> String? { 77 | guard let data = string.data(using: String.Encoding.utf8), 78 | let keyData = key.data(using: String.Encoding.utf8) 79 | else { return nil } 80 | 81 | return HMAC.generate(data: data as NSData, key: keyData as NSData, crypto: crypto)?.hexString 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Sources/Hash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hash.swift 3 | // CommonCryptoSwift 4 | // 5 | // Created by Khoa Pham on 07/05/16. 6 | // Copyright © 2016 Fantageek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CCommonCrypto 11 | 12 | public struct Hash { 13 | 14 | // MARK: - NSData 15 | 16 | public static func MD2(data: NSData) -> NSData { 17 | return Hash.hash(data: data, crypto: .MD2) 18 | } 19 | 20 | public static func MD4(data: NSData) -> NSData { 21 | return Hash.hash(data: data, crypto: .MD4) 22 | } 23 | 24 | public static func MD5(data: NSData) -> NSData { 25 | return Hash.hash(data: data, crypto: .MD5) 26 | } 27 | 28 | public static func SHA1(data: NSData) -> NSData { 29 | return Hash.hash(data: data, crypto: .SHA1) 30 | } 31 | 32 | public static func SHA224(data: NSData) -> NSData { 33 | return Hash.hash(data: data, crypto: .SHA224) 34 | } 35 | 36 | public static func SHA256(data: NSData) -> NSData { 37 | return Hash.hash(data: data, crypto: .SHA256) 38 | } 39 | 40 | public static func SHA384(data: NSData) -> NSData { 41 | return Hash.hash(data: data, crypto: .SHA384) 42 | } 43 | 44 | public static func SHA512(data: NSData) -> NSData { 45 | return Hash.hash(data: data, crypto: .SHA512) 46 | } 47 | 48 | static func hash(data: NSData, crypto: Crypto) -> NSData { 49 | var buffer = Array(repeating: 0, count: Int(crypto.length)) 50 | let _ = crypto.method(data.bytes, UInt32(data.length), &buffer) 51 | 52 | return NSData(bytes: buffer, length: buffer.count) 53 | } 54 | 55 | // MARK: - String 56 | 57 | public static func MD2(_ string: String) -> String? { 58 | return Hash.hash(string: string, crypto: .MD2) 59 | } 60 | 61 | public static func MD4(_ string: String) -> String? { 62 | return Hash.hash(string: string, crypto: .MD4) 63 | } 64 | 65 | public static func MD5(_ string: String) -> String? { 66 | return Hash.hash(string: string, crypto: .MD5) 67 | } 68 | 69 | public static func SHA1(_ string: String) -> String? { 70 | return Hash.hash(string: string, crypto: .SHA1) 71 | } 72 | 73 | public static func SHA224(_ string: String) -> String? { 74 | return Hash.hash(string: string, crypto: .SHA224) 75 | } 76 | 77 | public static func SHA256(_ string: String) -> String? { 78 | return Hash.hash(string: string, crypto: .SHA256) 79 | } 80 | 81 | public static func SHA384(_ string: String) -> String? { 82 | return Hash.hash(string: string, crypto: .SHA384) 83 | } 84 | 85 | public static func SHA512(_ string: String) -> String? { 86 | return Hash.hash(string: string, crypto: .SHA512) 87 | } 88 | 89 | static func hash(string: String, crypto: Crypto) -> String? { 90 | guard let data = string.data(using: String.Encoding.utf8) else { return nil } 91 | 92 | return Hash.hash(data: data as NSData, crypto: crypto).hexString 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Dependencies/CommonCrypto.swift/Sources/NSData+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSData+Extensions.swift 3 | // CommonCryptoSwift 4 | // 5 | // Created by Khoa Pham on 08/05/16. 6 | // Copyright © 2016 Fantageek. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension NSData { 12 | 13 | var hexString: String { 14 | var result = "" 15 | 16 | var bytes = [UInt8](repeating: 0, count: length) 17 | getBytes(&bytes, length: length) 18 | 19 | for byte in bytes { 20 | result += String(format: "%02x", UInt(byte)) 21 | } 22 | 23 | return result 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jared Sinclair 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sodes-audio-example 2 | 3 | An example AVAssetResourceLoaderDelegate implementation. A variation of this will be used in **’sodes**, a podcast app I'm working on. 4 | 5 | This repo accompanies a blog post [which can be found here](http://blog.jaredsinclair.com/post/149892449150/avassetresourceloaderdelegate). 6 | 7 | You are welcome to use this code as allowed under the generous terms of the MIT License, but **this code is not intended to be used as a re-usable library**. It's highly optimized for the needs of my particular app. I'm sharing it here for the benefit of anyone who's looking for an example of how to write an AVAssetResourceLoaderDelegate implementation. 8 | 9 | ## What It Does 10 | 11 | Contains an example implementation of an AVAssetResourceLoaderDelegate which downloads the requested byte ranges to a "scratch file" of locally-cached byte ranges. It also re-uses previously-downloaded byte ranges from that scratch file to service future requests that overlap the downloaded byte ranges, both during the current app session and in future sessions. This helps limit the number of times the same bytes are downloaded when streaming a podcast episode over more than one app session. Ideally each byte should never be downloaded more than once. 12 | 13 | When a request for a byte range is sent to the resource loader delegate, an array of "subrequests" is formed which are either scratch file requests or network requests. Scratch file requests read the data from existing byte ranges in the scratch file which have already been downloaded. Network requests are made for any gaps in the scratch file. The results of network requests are both passed to the AVAssetResourceLoader and written to the scratch file to be re-used later if the need arises. 14 | 15 | ## TL;DR Files 16 | 17 | - [ResourceLoaderDelegate.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/ResourceLoaderDelegate.swift) 18 | - [DataRequestLoader.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/DataRequestLoader.swift) 19 | - [ResourceLoaderSubrequest.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/ResourceLoaderSubrequest.swift) 20 | - [PlaybackController.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/PlaybackController.swift) 21 | 22 | ## Sample App Screenshot 23 | 24 | This repository also contains an example application so you can see it in action. 25 | 26 | You can see below some basic play controls, as well as a text view that prints out the byte ranges that have been successfully written to the current scratch file. 27 | 28 | Delete and reinstall the app to clear out the scratch file (or change the hard-coded MP3 URL to some other MP3 url and rebuild and run). 29 | 30 | 31 | 32 | ## Acknowledgements 33 | 34 | Contains a modified copy of [CommonCryptoSwift](https://github.com/onmyway133/Arcane), a convenient wrapper around CommonCrypto that can be used in a Swift framework. 35 | -------------------------------------------------------------------------------- /Sodes/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Sodes/.DS_Store -------------------------------------------------------------------------------- /Sodes/Sodes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sodes/Sodes.xcodeproj/project.xcworkspace/xcshareddata/Cast.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "0E1AB7D483F5968C839C9A65A2CE488A417D1236", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : 0, 8 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : 0 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "CB11FFCE-794D-4414-98CB-06B53A512BCD", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : "cast\/Dependencies\/CommonCrypto.swift\/", 13 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : "cast\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Cast", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Cast\/Cast.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/cast.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0E1AB7D483F5968C839C9A65A2CE488A417D1236" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/CommonCrypto.swift.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Sodes/Sodes.xcodeproj/project.xcworkspace/xcshareddata/Sodes.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : 0, 8 | "151CF6AE7A27E25C386CB7C06129C46E18BDA023" : 0, 9 | "EB2210CFD48672E403BED699D5D7F01B844069CF" : 9223372036854775807, 10 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : 0, 11 | "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9" : 9223372036854775807 12 | }, 13 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "E9EE63F7-A4C5-4163-BAA4-8C55549B40B3", 14 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 15 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : "sodes-sample\/Dependencies\/CommonCrypto.swift\/", 16 | "151CF6AE7A27E25C386CB7C06129C46E18BDA023" : "sodes-sample\/Dependencies\/JTSReachability\/", 17 | "EB2210CFD48672E403BED699D5D7F01B844069CF" : "sodes-sample\/Dependencies\/SWXMLHash\/", 18 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : "sodes-sample\/", 19 | "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9" : "sodes-audio-example\/" 20 | }, 21 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Sodes", 22 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 23 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Sodes\/Sodes.xcodeproj", 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 25 | { 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/cast.git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 28 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0E1AB7D483F5968C839C9A65A2CE488A417D1236" 29 | }, 30 | { 31 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/JTSReachability.git", 32 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 33 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "151CF6AE7A27E25C386CB7C06129C46E18BDA023" 34 | }, 35 | { 36 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/SWXMLHash.git", 37 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 38 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EB2210CFD48672E403BED699D5D7F01B844069CF" 39 | }, 40 | { 41 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/CommonCrypto.swift.git", 42 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 43 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" 44 | }, 45 | { 46 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/sodes-audio-example.git", 47 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 48 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /Sodes/Sodes.xcodeproj/xcshareddata/xcschemes/SodesAudio.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /Sodes/Sodes.xcodeproj/xcshareddata/xcschemes/SodesFoundation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /Sodes/Sodes.xcodeproj/xcshareddata/xcschemes/SwiftableFileHandle.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Sodes/SodesAudio/.DS_Store -------------------------------------------------------------------------------- /Sodes/SodesAudio/AVContentInfoRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVContentInfoRequest.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 7/24/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import MobileCoreServices 12 | 13 | internal extension AVAssetResourceLoadingContentInformationRequest { 14 | 15 | func update(with response: URLResponse) { 16 | 17 | if let response = response as? HTTPURLResponse { 18 | 19 | // TODO: [MEDIUM] Obtain the actual content type. 20 | contentType = "public.mp3" 21 | 22 | if let length = response.sodes_expectedContentLength { 23 | contentLength = length 24 | } 25 | 26 | if let acceptRanges = response.allHeaderFields["Accept-Ranges"] as? String, 27 | acceptRanges == "bytes" 28 | { 29 | isByteRangeAccessSupported = true 30 | } else { 31 | isByteRangeAccessSupported = false 32 | } 33 | } 34 | else { 35 | assertionFailure("Invalid URL Response.") 36 | } 37 | } 38 | 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/ByteRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ByteRange.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 7/24/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Summable: Comparable { 12 | static func +(lhs: Self, rhs: Self) -> Self 13 | static func -(lhs: Self, rhs: Self) -> Self 14 | func decremented() -> Self 15 | func toInt() -> Int 16 | } 17 | 18 | extension Int64: Summable { 19 | func decremented() -> Int64 { 20 | return self - 1 21 | } 22 | func toInt() -> Int { 23 | return Int(self) 24 | } 25 | } 26 | 27 | public typealias ByteRange = Range 28 | 29 | enum ByteRangeIndexPosition { 30 | case before 31 | case inside 32 | case after 33 | } 34 | 35 | extension Range where Bound: Summable { 36 | 37 | var length: Bound { 38 | return upperBound - lowerBound 39 | } 40 | 41 | var lastValidIndex: Bound { 42 | return upperBound.decremented() 43 | } 44 | 45 | var subdataRange: Range { 46 | return lowerBound.toInt()..<(upperBound.toInt()) 47 | } 48 | 49 | func leadingIntersection(in otherRange: Range) -> Range? { 50 | if lowerBound <= otherRange.lowerBound && lastValidIndex >= otherRange.lowerBound { 51 | if lastValidIndex > otherRange.lastValidIndex { 52 | return otherRange 53 | } else { 54 | let lowerBound = otherRange.lowerBound 55 | let upperBound = otherRange.lowerBound + length - otherRange.lowerBound 56 | return (lowerBound.. Range? { 64 | if let leading = leadingIntersection(in: otherRange), !fullySatisfies(otherRange) { 65 | return ((otherRange.lowerBound + leading.length).. Bool { 72 | if let intersection = leadingIntersection(in: requestedRange) { 73 | return intersection == requestedRange 74 | } else { 75 | return false 76 | } 77 | } 78 | 79 | func intersects(_ otherRange: Range) -> Bool { 80 | return otherRange.lowerBound < upperBound && lowerBound < otherRange.upperBound 81 | } 82 | 83 | func isContiguousWith(_ otherRange: Range) -> Bool { 84 | if otherRange.upperBound == lowerBound { 85 | return true 86 | } else if upperBound == otherRange.lowerBound { 87 | return true 88 | } else { 89 | return false 90 | } 91 | } 92 | 93 | func relativePosition(of index: Bound) -> ByteRangeIndexPosition { 94 | if index < lowerBound { 95 | return .before 96 | } else if index >= upperBound { 97 | return .after 98 | } else { 99 | return .inside 100 | } 101 | } 102 | 103 | } 104 | 105 | func combine(_ ranges: [ByteRange]) -> [ByteRange] { 106 | var combinedRanges = [ByteRange]() 107 | let uncheckedRanges = ranges.sorted{$0.length > $1.length} 108 | for uncheckedRange in uncheckedRanges { 109 | let intersectingRanges = combinedRanges.filter{ 110 | $0.intersects(uncheckedRange) || $0.isContiguousWith(uncheckedRange) 111 | } 112 | if intersectingRanges.isEmpty { 113 | combinedRanges.append(uncheckedRange) 114 | } else { 115 | for range in intersectingRanges { 116 | if let index = combinedRanges.index(of: range) { 117 | combinedRanges.remove(at: index) 118 | } 119 | } 120 | let combinedRange = intersectingRanges.reduce(uncheckedRange, +) 121 | combinedRanges.append(combinedRange) 122 | } 123 | } 124 | return combinedRanges.sorted{$0.lowerBound < $1.lowerBound} 125 | } 126 | 127 | /// Adding byte ranges is currently very naive. It takes the lowest lowerBound 128 | /// and the highest upper bound and computes a range between the two. It assumes 129 | /// that the programmer desires this behavior, for instance, when you're adding 130 | /// a sequence of byte ranges which form a continuous range when summed as a 131 | /// whole even though any two random members might not overlap or be contiguous. 132 | private func +(lhs: ByteRange, rhs: ByteRange) -> ByteRange { 133 | let lowerBound = min(lhs.lowerBound, rhs.lowerBound) 134 | let upperBound = max(lhs.upperBound, rhs.upperBound) 135 | return (lowerBound.. Data? { 18 | if Int64(count) >= range.length { 19 | return subdata(in: (0.. [Operation] { 114 | return subrequests.map { (subrequest) -> Operation in 115 | switch subrequest.source { 116 | case .scratchFile: 117 | return newScratchFileOperation(for: subrequest.range) 118 | case .network: 119 | return newNetworkRequestOperation(for: resourceUrl, range: subrequest.range) 120 | } 121 | } 122 | } 123 | 124 | /// Creates an operation which will load `range` from the scratch file. 125 | fileprivate func newScratchFileOperation(for range: ByteRange) -> Operation { 126 | SodesLog("Creating operation for scratch file for range: \(range)") 127 | return BlockOperation { [weak self] in 128 | guard let this = self else {return} 129 | guard !this.cancelled && !this.failed else {return} 130 | do { 131 | let data = try this.scratchFileHandle.read(from: range) 132 | this.currentOffset = range.upperBound 133 | this.callbackQueue.sync { [weak this] in 134 | guard let this = this else {return} 135 | guard !this.cancelled && !this.failed else {return} 136 | SodesLog("Sucessfully read data from the scratch file: \(range)") 137 | this.delegate?.dataRequestLoader(this, didReceive: data, forSubrange: range) 138 | } 139 | } catch (let e) { 140 | SodesLog(e) 141 | this.fail(with: e) 142 | } 143 | } 144 | } 145 | 146 | /// Creates an operation which will download `range` from `url`. 147 | fileprivate func newNetworkRequestOperation(for url: URL, range: ByteRange) -> Operation { 148 | SodesLog("Creating operation for byte range: \(range).") 149 | return DelegatedHTTPOperation( 150 | request: URLRequest.dataRequest(from: url, for: range), 151 | configuration: .default, 152 | dataDelegate: self, 153 | delegateQueue: httpCallbackQueue, 154 | completion: { [weak self] (result) in 155 | guard let this = self else {return} 156 | guard !this.cancelled && !this.failed else {return} 157 | switch result { 158 | case .success(_, let bytesExpected, let bytesReceived): 159 | SodesLog("bytesExpected: \(bytesExpected), bytesReceived: \(bytesReceived)") 160 | // No-op, just let the procedure continue onto the next operation 161 | break 162 | case .error(let response,let error): 163 | SodesLog("\(error): \(response)") 164 | this.fail(with: error) 165 | } 166 | }) 167 | } 168 | 169 | /// Safely moves the request loader into its failed state. 170 | fileprivate func fail(with error: Error?) { 171 | failed = true 172 | operationQueue.cancelAllOperations() 173 | operationQueue.addOperation { [weak self] in 174 | guard let this = self else {return} 175 | guard !this.cancelled else {return} 176 | this.callbackQueue.async { 177 | this.delegate?.dataRequestLoader(this, didFailWithError: error) 178 | } 179 | } 180 | } 181 | 182 | } 183 | 184 | extension DataRequestLoader: HTTPOperationDataDelegate { 185 | 186 | /// Will be called when the operation receives a success-level response. 187 | /// This gives the receiver a chance to respond to it in case there's a 188 | /// domain-specific error. 189 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveResponse response: HTTPURLResponse) { 190 | 191 | assert(OperationQueue.current === httpCallbackQueue) 192 | 193 | // As of this writing, DelegatedHTTPOperation should treat status codes 194 | // outside this 200-level range as errors, not calling this delegate 195 | // method but finishing with an error. 196 | assert(200 <= response.statusCode && response.statusCode <= 299) 197 | 198 | if !response.sodes_isByteRangeEnabledResponse { 199 | assertionFailure("Byte ranges not supported by this server. What gives?") 200 | fail(with: SodesAudioError.byteRangeAccessNotSupported(response)) 201 | } 202 | 203 | } 204 | 205 | /// This will be called many times a second as chunks of data are loaded. 206 | /// The receiver will write the data to the scratch file and, if successful, 207 | /// will notify its delegate of the loaded data. The receiver will pass the 208 | /// data onto AVFoundation and update the metadata registry of loaded 209 | /// byte ranges. 210 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveData data: Data) { 211 | 212 | assert(OperationQueue.current === httpCallbackQueue) 213 | 214 | do { 215 | try scratchFileHandle.write(data, at: UInt64(currentOffset)) 216 | scratchFileHandle.synchronizeFile() 217 | let range: ByteRange = (currentOffset.. 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 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/MiscExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiscExtensions.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 7/16/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | internal let preferredTimescale: Int32 = 10000 13 | 14 | internal extension UInt { 15 | static func withInterval(_ i: TimeInterval?) -> UInt? { 16 | if let i = i { 17 | return UInt(i) 18 | } else { 19 | return nil 20 | } 21 | } 22 | } 23 | 24 | internal extension TimeInterval { 25 | var asCMTime: CMTime { 26 | return CMTimeMakeWithSeconds(self, preferredTimescale) 27 | } 28 | } 29 | 30 | internal extension CMTime { 31 | var asTimeInterval: TimeInterval? { 32 | if isNumeric { 33 | return CMTimeGetSeconds(self) 34 | } else { 35 | return nil 36 | } 37 | } 38 | } 39 | 40 | internal extension AVPlayer { 41 | 42 | var isPlaying: Bool { 43 | return error == nil && rate != 0 && currentItem != nil 44 | } 45 | 46 | var isPaused: Bool { 47 | return error == nil && rate == 0 && currentItem != nil 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/NSObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 7/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | internal extension NSObject { 12 | 13 | func add(observer: NSObject, for keypaths: [String], options: NSKeyValueObservingOptions = .new, context: UnsafeMutableRawPointer) { 14 | for path in keypaths { 15 | addObserver(observer, forKeyPath: path, options: options, context: context) 16 | } 17 | } 18 | 19 | func remove(observer: NSObject, for keypaths: [String], options: NSKeyValueObservingOptions = .new, context: UnsafeMutableRawPointer) { 20 | for path in keypaths { 21 | removeObserver(observer, forKeyPath: path, context: context) 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/PlaybackSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaybackSource.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 7/16/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import MediaPlayer 12 | import SodesFoundation 13 | 14 | public protocol PlaybackSource { 15 | var uniqueId: String {get} 16 | var artistId: String {get} 17 | var remoteUrl: URL {get} 18 | var title: String? {get} 19 | var albumTitle: String? {get} 20 | var artist: String? {get} 21 | var artworkUrl: URL? {get} 22 | var mediaType: MPMediaType {get} 23 | var expectedLengthInBytes: Int64? {get} 24 | } 25 | 26 | internal extension PlaybackSource { 27 | 28 | func nowPlayingInfo(image: UIImage? = nil, duration: TimeInterval? = nil, elapsedPlaybackTime: TimeInterval? = nil, rate: Double? = nil) -> [String: AnyObject] { 29 | 30 | var info: [String: AnyObject] = [:] 31 | 32 | info[MPMediaItemPropertyMediaType] = NSNumber(value: mediaType.rawValue) 33 | 34 | if let title = title { 35 | info[MPMediaItemPropertyTitle] = title as NSString 36 | } 37 | if let albumTitle = albumTitle { 38 | info[MPMediaItemPropertyAlbumTitle] = albumTitle as NSString 39 | } 40 | if let artist = artist { 41 | info[MPMediaItemPropertyArtist] = artist as NSString 42 | info[MPMediaItemPropertyAlbumArtist] = artist as NSString 43 | } 44 | if let image = image { 45 | let artwork = MPMediaItemArtwork(boundsSize: image.size) { (inputSize) -> UIImage in 46 | return image.draw(at: inputSize) 47 | } 48 | info[MPMediaItemPropertyArtwork] = artwork 49 | } 50 | if let duration = UInt.withInterval(duration) { 51 | info[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration) 52 | } 53 | if let time = elapsedPlaybackTime { 54 | info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: time) 55 | } 56 | if let rate = rate { 57 | info[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: rate) 58 | } 59 | 60 | return info 61 | } 62 | 63 | } 64 | 65 | private extension UIImage { 66 | 67 | func draw(at targetSize: CGSize) -> UIImage { 68 | 69 | guard !self.size.equalTo(CGSize.zero) else { 70 | SodesLog("Invalid image size: (0,0)") 71 | return self 72 | } 73 | 74 | guard !targetSize.equalTo(CGSize.zero) else { 75 | SodesLog("Invalid target size: (0,0)") 76 | return self 77 | } 78 | 79 | let scaledSize = sizeThatFills(targetSize) 80 | let x = (targetSize.width - scaledSize.width) / 2.0 81 | let y = (targetSize.height - scaledSize.height) / 2.0 82 | let drawingRect = CGRect(x: x, y: y, width: scaledSize.width, height: scaledSize.height) 83 | 84 | UIGraphicsBeginImageContextWithOptions(targetSize, true, 0) 85 | draw(in: drawingRect) 86 | let resizedImage = UIGraphicsGetImageFromCurrentImageContext() 87 | UIGraphicsEndImageContext() 88 | 89 | return resizedImage! 90 | 91 | } 92 | 93 | func sizeThatFills(_ other: CGSize) -> CGSize { 94 | guard !size.equalTo(CGSize.zero) else { 95 | return other 96 | } 97 | let heightRatio = other.height / size.height 98 | let widthRatio = other.width / size.width 99 | if heightRatio > widthRatio { 100 | return CGSize(width: size.width * heightRatio, height: size.height * heightRatio) 101 | } else { 102 | return CGSize(width: size.width * widthRatio, height: size.height * widthRatio) 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/Requests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Requests.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 8/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | protocol Request: class { 13 | var resourceUrl: URL {get} 14 | var loadingRequest: AVAssetResourceLoadingRequest {get} 15 | 16 | func cancel() 17 | } 18 | 19 | class ContentInfoRequest: Request { 20 | 21 | let resourceUrl: URL 22 | let loadingRequest: AVAssetResourceLoadingRequest 23 | let infoRequest: AVAssetResourceLoadingContentInformationRequest 24 | let task: URLSessionTask 25 | 26 | init(resourceUrl: URL, loadingRequest: AVAssetResourceLoadingRequest, infoRequest: AVAssetResourceLoadingContentInformationRequest, task: URLSessionTask) { 27 | self.resourceUrl = resourceUrl 28 | self.loadingRequest = loadingRequest 29 | self.infoRequest = infoRequest 30 | self.task = task 31 | } 32 | 33 | func cancel() { 34 | task.cancel() 35 | if !loadingRequest.isCancelled && !loadingRequest.isFinished { 36 | loadingRequest.finishLoading() 37 | } 38 | } 39 | 40 | } 41 | 42 | class DataRequest: Request { 43 | 44 | let resourceUrl: URL 45 | let loadingRequest: AVAssetResourceLoadingRequest 46 | let dataRequest: AVAssetResourceLoadingDataRequest 47 | let loader: DataRequestLoader 48 | 49 | init(resourceUrl: URL, loadingRequest: AVAssetResourceLoadingRequest, dataRequest: AVAssetResourceLoadingDataRequest, loader: DataRequestLoader) { 50 | self.resourceUrl = resourceUrl 51 | self.loadingRequest = loadingRequest 52 | self.dataRequest = dataRequest 53 | self.loader = loader 54 | } 55 | 56 | func cancel() { 57 | loader.cancel() 58 | if !loadingRequest.isCancelled && !loadingRequest.isFinished { 59 | loadingRequest.finishLoading() 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/ResourceLoaderSubrequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceLoaderSubrequest.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 8/5/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SodesFoundation 11 | 12 | /// An intermediate struct useful for calculating which subranges of data can be 13 | /// read from a scratch file versus downloaded from the Internet. 14 | struct ResourceLoaderSubrequest: Equatable, CustomStringConvertible { 15 | 16 | /// The sources from which data can be read. 17 | enum Source { 18 | case scratchFile 19 | case network 20 | } 21 | 22 | /// The source from which the data must be read. 23 | let source: Source 24 | 25 | /// The subrequest range (only a portion of the entire requested range). 26 | let range: ByteRange 27 | 28 | /// Description for debugging. 29 | var description: String { 30 | var s = "ResourceLoaderSubrequest(" 31 | switch source { 32 | case .scratchFile: 33 | s += ".scratchFile" 34 | case .network: 35 | s += ".network" 36 | } 37 | s += ", " 38 | s += range.description 39 | s += ")" 40 | return s 41 | } 42 | } 43 | 44 | func ==(lhs: ResourceLoaderSubrequest, rhs: ResourceLoaderSubrequest) -> Bool { 45 | return lhs.source == rhs.source && lhs.range == rhs.range 46 | } 47 | 48 | extension ResourceLoaderSubrequest { 49 | 50 | /// Creates an array of subrequests. It will create scratch file subrequests 51 | /// for data already loaded in `scratchFileRanges`, and network subrequests 52 | /// for any gaps in the requested range not found in `scratchFileRanges`. 53 | static func subrequests(requestedRange: ByteRange, scratchFileRanges: [ByteRange]) -> [ResourceLoaderSubrequest] { 54 | 55 | var subrequests: [ResourceLoaderSubrequest] = [] 56 | 57 | let intersectingRanges = scratchFileRanges 58 | .filter{$0.intersects(requestedRange)} 59 | .sorted{$0.lowerBound < $1.lowerBound} 60 | 61 | if intersectingRanges.isEmpty { 62 | let networkSubrequest = ResourceLoaderSubrequest(source: .network, range: requestedRange) 63 | subrequests.append(networkSubrequest) 64 | return subrequests 65 | } 66 | 67 | var cursor = requestedRange.lowerBound 68 | 69 | var nextRangeIndex = 0 70 | 71 | while cursor < requestedRange.upperBound && nextRangeIndex < intersectingRanges.count { 72 | 73 | let nextRange = intersectingRanges[nextRangeIndex] 74 | let position = nextRange.relativePosition(of: cursor) 75 | 76 | switch position { 77 | case .before: 78 | let lowerBound = cursor 79 | let upperBound = min(nextRange.lowerBound, requestedRange.upperBound) 80 | let networkRequest = ResourceLoaderSubrequest( 81 | source: .network, 82 | range: (lowerBound.. Data { 16 | do { 17 | let data = try readData( 18 | fromLocation: UInt64(range.lowerBound), 19 | length: UInt64(range.length) 20 | ) 21 | return data 22 | } catch { 23 | throw(error) 24 | } 25 | } 26 | 27 | func write(_ data: Data, over range: ByteRange) throws { 28 | do { 29 | try write(data, at: UInt64(range.lowerBound)) 30 | } catch { 31 | throw(error) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/ScratchFileInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScratchFileInfo.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 8/16/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftableFileHandle 11 | import SodesFoundation 12 | 13 | /// Info for the current scratch file for ResourceLoaderDelegate. 14 | class ScratchFileInfo { 15 | 16 | /// Cache validation info. 17 | struct CacheInfo { 18 | let contentLength: Int64? 19 | let etag: String? 20 | let lastModified: Date? 21 | } 22 | 23 | /// The remote URL from which the file should be downloaded. 24 | let resourceUrl: URL 25 | 26 | /// The subdirectory for this scratch file and its metadata. 27 | let directory: URL 28 | 29 | /// The file url to the scratch file itself. 30 | let scratchFileUrl: URL 31 | 32 | /// The file url to the metadata for the scratch file. 33 | let metaDataUrl: URL 34 | 35 | /// The file handle used for reading/writing bytes to the scratch file. 36 | let fileHandle: SODSwiftableFileHandle 37 | 38 | /// The most recent cache validation info. 39 | var cacheInfo: CacheInfo 40 | 41 | /// The byte ranges for the scratch file that have been saved thus far. 42 | var loadedByteRanges: [ByteRange] 43 | 44 | /// Designated initializer. 45 | init(resourceUrl: URL, directory: URL, scratchFileUrl: URL, metaDataUrl: URL, fileHandle: SODSwiftableFileHandle, cacheInfo: CacheInfo, loadedByteRanges: [ByteRange]) { 46 | self.resourceUrl = resourceUrl 47 | self.directory = directory 48 | self.scratchFileUrl = scratchFileUrl 49 | self.metaDataUrl = metaDataUrl 50 | self.fileHandle = fileHandle 51 | self.cacheInfo = cacheInfo 52 | self.loadedByteRanges = loadedByteRanges 53 | } 54 | 55 | } 56 | 57 | extension ScratchFileInfo.CacheInfo { 58 | 59 | static let none = ScratchFileInfo.CacheInfo( 60 | contentLength: nil, etag: nil, lastModified: nil 61 | ) 62 | 63 | func isStillValid(comparedTo otherInfo: ScratchFileInfo.CacheInfo) -> Bool { 64 | if let old = self.etag, let new = otherInfo.etag { 65 | return old == new 66 | } 67 | else if let old = lastModified, let new = otherInfo.lastModified { 68 | return old == new 69 | } 70 | else { 71 | return false 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/ScratchFileInfoSerialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScratchFileInfoSerialization.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 8/5/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SodesFoundation 11 | 12 | extension PropertyListSerialization { 13 | 14 | /// Converts the input values into a data object that can be serialized as a 15 | /// property list. 16 | static func representation(for byteRanges: [ByteRange], cacheInfo: ScratchFileInfo.CacheInfo) -> Data? { 17 | var plist: [String: AnyObject] = [ 18 | "byteRanges": byteRanges.map{stringRepresentation(for: $0)} as NSArray 19 | ] 20 | if let length = cacheInfo.contentLength { 21 | plist["contentLength"] = NSNumber(value: length) 22 | } 23 | if let etag = cacheInfo.etag { 24 | plist["etag"] = etag as NSString 25 | } 26 | if let lastModified = cacheInfo.lastModified { 27 | plist["lastModified"] = lastModified as NSDate 28 | } 29 | return try? data(fromPropertyList: plist, format: .xml, options: 0) 30 | } 31 | 32 | /// Converts property list data into byte ranges and cache info. 33 | static func byteRangesAndCacheInfo(from representation: Data) -> ([ByteRange], ScratchFileInfo.CacheInfo)? { 34 | guard 35 | let value = try? propertyList(from: representation, format: nil), let plist = value as? [String: Any], 36 | let strings = plist["byteRanges"] as? [String] 37 | else 38 | { 39 | return nil 40 | } 41 | let byteRanges = strings.flatMap{range(from: $0)} 42 | let info = ScratchFileInfo.CacheInfo( 43 | contentLength: (plist["contentLength"] as? NSNumber)?.int64Value, 44 | etag: plist["etag"] as? String, 45 | lastModified: plist["lastModified"] as? Date 46 | ) 47 | return (byteRanges, info) 48 | } 49 | 50 | /// Convenience method. 51 | private static func stringRepresentation(for range: ByteRange) -> String { 52 | return "\(range.lowerBound)..<\(range.upperBound)" 53 | } 54 | 55 | /// Convenience method. 56 | private static func range(from string: String) -> ByteRange? { 57 | let comps = string.components(separatedBy: "..<") 58 | if comps.count == 2 { 59 | if let l = Int64(comps[0]), let u = Int64(comps[1]) { 60 | return (l.. Bool { 72 | if let data = PropertyListSerialization.representation(for: byteRanges, cacheInfo: cacheInfo) { 73 | do { 74 | try data.write(to: fileUrl, options: .atomic) 75 | return true 76 | } catch { 77 | SodesLog(error) 78 | return false 79 | } 80 | } else { 81 | return false 82 | } 83 | } 84 | 85 | /// Reads the values from a property list found at `fileUrl`. 86 | func readRanges(at fileUrl: URL) -> ([ByteRange], ScratchFileInfo.CacheInfo)? { 87 | if let data = try? Data(contentsOf: fileUrl) { 88 | return PropertyListSerialization.byteRangesAndCacheInfo(from: data) 89 | } else { 90 | return nil 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 7/24/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | internal extension URL { 12 | 13 | /// Returns true if the receiver's path extension is equal to `pathExt`. 14 | func hasPathExtension(_ pathExt: String) -> Bool { 15 | guard let comps = URLComponents(url: self, resolvingAgainstBaseURL: false) else {return false} 16 | return (comps.path as NSString).pathExtension == pathExt 17 | } 18 | 19 | /// Adds the scheme prefix to a copy of the receiver. 20 | func convertToRedirectURL(prefix: String) -> URL? { 21 | guard var comps = URLComponents(url: self, resolvingAgainstBaseURL: false) else {return nil} 22 | guard let scheme = comps.scheme else {return nil} 23 | comps.scheme = prefix + scheme 24 | return comps.url 25 | } 26 | 27 | /// Removes the scheme prefix from a copy of the receiver. 28 | func convertFromRedirectURL(prefix: String) -> URL? { 29 | guard var comps = URLComponents(url: self, resolvingAgainstBaseURL: false) else {return nil} 30 | guard let scheme = comps.scheme else {return nil} 31 | comps.scheme = scheme.replacingOccurrences(of: prefix, with: "") 32 | return comps.url 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/URLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 8/19/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | 12 | extension AVAssetResourceLoadingDataRequest { 13 | 14 | var byteRange: ByteRange { 15 | let lowerBound = requestedOffset 16 | let upperBound = (lowerBound + requestedLength - 1) 17 | return (lowerBound.. URLRequest { 47 | var request = URLRequest(url: url) 48 | request.setByteRangeHeader(for: range) 49 | request.cachePolicy = .reloadIgnoringLocalCacheData 50 | return request 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sodes/SodesAudio/URLResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLResponse.swift 3 | // SodesAudio 4 | // 5 | // Created by Jared Sinclair on 8/20/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SodesFoundation 11 | 12 | extension URLResponse { 13 | 14 | func cacheInfo(using formatter: RFC822Formatter) -> ScratchFileInfo.CacheInfo { 15 | return ScratchFileInfo.CacheInfo( 16 | contentLength: sodes_expectedContentLength, 17 | etag: etag, 18 | lastModified: lastModified(using: formatter) 19 | ) 20 | } 21 | 22 | var sodes_isByteRangeEnabledResponse: Bool { 23 | return sodes_responseRange != nil 24 | } 25 | 26 | var sodes_responseRange: ByteRange? { 27 | guard let response = self as? HTTPURLResponse else {return nil} 28 | if let fullString = response.allHeaderFields["Content-Range"] as? String, 29 | let firstPart = fullString.characters.split(separator: "/").map({String($0)}).first 30 | { 31 | if let prefixRange = firstPart.range(of: "bytes ") { 32 | let rangeString = firstPart.substring(from: prefixRange.upperBound) 33 | let comps = rangeString.components(separatedBy: "-") 34 | let ints = comps.flatMap{Int64($0)} 35 | if ints.count == 2 { 36 | return (ints[0]..<(ints[1]+1)) 37 | } 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | var sodes_expectedContentLength: Int64? { 44 | guard let response = self as? HTTPURLResponse else {return nil} 45 | if let rangeString = response.allHeaderFields["Content-Range"] as? String, 46 | let bytesString = rangeString.characters.split(separator: "/").map({String($0)}).last, 47 | let bytes = Int64(bytesString) 48 | { 49 | return bytes 50 | } else { 51 | return nil 52 | } 53 | } 54 | 55 | var etag: String? { 56 | guard let response = self as? HTTPURLResponse else {return nil} 57 | return response.allHeaderFields["Etag"] as? String 58 | } 59 | 60 | func lastModified(using formatter: RFC822Formatter) -> Date? { 61 | guard let response = self as? HTTPURLResponse else {return nil} 62 | if let string = response.allHeaderFields["Last-Modified"] as? String { 63 | return formatter.date(from: string) 64 | } else { 65 | return nil 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/ByteRangeSerializationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ByteRangeSerializationTests.swift 3 | // SodesAudioTests 4 | // 5 | // Created by Jared Sinclair on 8/5/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | import SodesFoundation 11 | @testable import SodesAudio 12 | 13 | class ByteRangeSerializationTests: XCTestCase { 14 | 15 | // MARK: JSON Serialization 16 | 17 | func testItConvertsByteRangesToData() { 18 | XCTAssertNotNil(PropertyListSerialization.representation(for: [], cacheInfo: .none)) 19 | XCTAssertNotNil(PropertyListSerialization.representation(for:[(0..<1)], cacheInfo: .none)) 20 | XCTAssertNotNil(PropertyListSerialization.representation(for:[(0..<1), (1..<2), (2..<3)], cacheInfo: .none)) 21 | } 22 | 23 | func testItConvertsByteRangesToDataAndBackAgain() { 24 | 25 | var input: [ByteRange]! 26 | var data: Data! 27 | var output: ([ByteRange], ScratchFileInfo.CacheInfo)! 28 | 29 | input = [] 30 | data = PropertyListSerialization.representation(for:input, cacheInfo: .none) 31 | output = PropertyListSerialization.byteRangesAndCacheInfo(from: data) 32 | XCTAssertEqual(input, output.0) 33 | 34 | input = [(0..<1)] 35 | data = PropertyListSerialization.representation(for:input, cacheInfo: .none) 36 | output = PropertyListSerialization.byteRangesAndCacheInfo(from: data) 37 | XCTAssertEqual(input, output.0) 38 | 39 | input = [(0..<1), (1..<2), (2..<3)] 40 | data = PropertyListSerialization.representation(for:input, cacheInfo: .none) 41 | output = PropertyListSerialization.byteRangesAndCacheInfo(from: data) 42 | XCTAssertEqual(input, output.0) 43 | 44 | } 45 | 46 | // MARK: File Management 47 | 48 | func testItSavesByteRangesAndReadsThemBack() { 49 | let directory = FileManager.default.cachesDirectory()! 50 | let fileUrl = directory.appendingPathComponent(UUID().uuidString, isDirectory: false) 51 | let inputRanges: [ByteRange] = [(0..<1), (3..<10)] 52 | let inputDate = Date() 53 | let contentLength: Int64 = 2056 54 | let inputInfo = ScratchFileInfo.CacheInfo( 55 | contentLength: contentLength, etag: "e", lastModified: inputDate 56 | ) 57 | XCTAssertTrue(FileManager.default.save(byteRanges: inputRanges, cacheInfo: inputInfo, to: fileUrl)) 58 | let output = FileManager.default.readRanges(at: fileUrl) 59 | XCTAssertEqual(contentLength, output!.1.contentLength) 60 | XCTAssertEqual(inputRanges, output!.0) 61 | XCTAssertEqual("e", output!.1.etag) 62 | XCTAssertEqualWithAccuracy(inputDate.timeIntervalSince(output!.1.lastModified!), 0.0, accuracy: 1.0) 63 | } 64 | 65 | func testItSavesByteRangesTwiceAndReadsThemBack() { 66 | 67 | let directory = FileManager.default.cachesDirectory()! 68 | let fileUrl = directory.appendingPathComponent(NSUUID().uuidString, isDirectory: false) 69 | let inputRanges1: [ByteRange] = [(0..<1), (3..<10)] 70 | let inputRanges2: [ByteRange] = [(0..<1), (16..<20), (3..<10)] 71 | let contentLength: Int64 = 2056 72 | let inputDate = Date() 73 | let inputInfo = ScratchFileInfo.CacheInfo( 74 | contentLength: contentLength, etag: "e", lastModified: inputDate 75 | ) 76 | 77 | XCTAssertTrue(FileManager.default.save(byteRanges: inputRanges1, cacheInfo: inputInfo, to: fileUrl)) 78 | let output1 = FileManager.default.readRanges(at: fileUrl) 79 | XCTAssertEqual(contentLength, output1!.1.contentLength) 80 | XCTAssertEqual(inputRanges1, output1!.0) 81 | XCTAssertEqual("e", output1!.1.etag) 82 | XCTAssertEqualWithAccuracy(inputDate.timeIntervalSince(output1!.1.lastModified!), 0.0, accuracy: 1.0) 83 | 84 | XCTAssertTrue(FileManager.default.save(byteRanges: inputRanges2, cacheInfo: inputInfo, to: fileUrl)) 85 | let output2 = FileManager.default.readRanges(at: fileUrl) 86 | XCTAssertEqual(contentLength, output2!.1.contentLength) 87 | XCTAssertEqual(inputRanges2, output2!.0) 88 | XCTAssertEqual("e", output2!.1.etag) 89 | XCTAssertEqualWithAccuracy(inputDate.timeIntervalSince(output2!.1.lastModified!), 0.0, accuracy: 1.0) 90 | 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/ByteRangeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ByteRangeTests.swift 3 | // SodesAudioTests 4 | // 5 | // Created by Jared Sinclair on 8/5/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesAudio 11 | 12 | class ByteRangeTests: XCTestCase { 13 | 14 | // MARK: General 15 | 16 | func testItCreatesAValidRange() { 17 | let range: ByteRange = (0..<10) 18 | XCTAssertEqual(range.lowerBound, 0) 19 | XCTAssertEqual(range.upperBound, 10) 20 | XCTAssertEqual(range.lastValidIndex, 9) 21 | XCTAssertEqual(range.subdataRange, Range((0..<10))) 22 | } 23 | 24 | // MARK: Leading 25 | 26 | func testItComputesCorrectLeadingIntersections_SameStarts_DifferingEnds() { 27 | 28 | let a1: ByteRange = (0..<5) 29 | let b1: ByteRange = (0..<5) 30 | let expected1: ByteRange? = (0..<5) 31 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1) 32 | 33 | let a2: ByteRange = (0..<3) 34 | let b2: ByteRange = (0..<5) 35 | let expected2: ByteRange? = (0..<3) 36 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2) 37 | 38 | let a3: ByteRange = (0..<10) 39 | let b3: ByteRange = (0..<5) 40 | let expected3: ByteRange? = (0..<5) 41 | XCTAssertEqual(a3.leadingIntersection(in: b3), expected3) 42 | 43 | let a4: ByteRange = (0..<5) 44 | let b4: ByteRange = (0..<10) 45 | let expected4: ByteRange? = (0..<5) 46 | XCTAssertEqual(a4.leadingIntersection(in: b4), expected4) 47 | 48 | } 49 | 50 | func testItComputesCorrectLeadingIntersections_DifferingStarts_SameEnds() { 51 | 52 | let a1: ByteRange = (5..<10) 53 | let b1: ByteRange = (0..<10) 54 | let expected1: ByteRange? = nil 55 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1) 56 | 57 | let a2: ByteRange = (0..<10) 58 | let b2: ByteRange = (5..<10) 59 | let expected2: ByteRange? = (5..<10) 60 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2) 61 | 62 | } 63 | 64 | func testItComputesCorrectLeadingIntersections_DifferingStarts_DifferingEnds() { 65 | 66 | let a1: ByteRange = (0..<20) 67 | let b1: ByteRange = (5..<10) 68 | let expected1: ByteRange? = (5..<10) 69 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1) 70 | 71 | let a2: ByteRange = (5..<10) 72 | let b2: ByteRange = (0..<20) 73 | let expected2: ByteRange? = nil 74 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2) 75 | 76 | let a3: ByteRange = (10..<20) 77 | let b3: ByteRange = (5..<10) 78 | let expected3: ByteRange? = nil 79 | XCTAssertEqual(a3.leadingIntersection(in: b3), expected3) 80 | 81 | let a4: ByteRange = (5..<10) 82 | let b4: ByteRange = (10..<20) 83 | let expected4: ByteRange? = nil 84 | XCTAssertEqual(a4.leadingIntersection(in: b4), expected4) 85 | 86 | } 87 | 88 | func testItComputesCorrectLeadingIntersections_OffByOnes() { 89 | 90 | let a1: ByteRange = (0..<2) 91 | let b1: ByteRange = (1..<2) 92 | let expected1: ByteRange? = (1..<2) 93 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1) 94 | 95 | let a2: ByteRange = (1..<2) 96 | let b2: ByteRange = (0..<2) 97 | let expected2: ByteRange? = nil 98 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2) 99 | 100 | let a3: ByteRange = (1..<2) 101 | let b3: ByteRange = (0..<1) 102 | let expected3: ByteRange? = nil 103 | XCTAssertEqual(a3.leadingIntersection(in: b3), expected3) 104 | 105 | let a4: ByteRange = (0..<1) 106 | let b4: ByteRange = (1..<2) 107 | let expected4: ByteRange? = nil 108 | XCTAssertEqual(a4.leadingIntersection(in: b4), expected4) 109 | 110 | } 111 | 112 | // MARK: Trailing 113 | 114 | func testItComputesCorrectTrailingRanges_SameStarts_DifferingEnds() { 115 | 116 | let a1: ByteRange = (0..<5) 117 | let b1: ByteRange = (0..<5) 118 | let expected1: ByteRange? = nil 119 | XCTAssertEqual(a1.trailingRange(in: b1), expected1) 120 | 121 | let a2: ByteRange = (0..<3) 122 | let b2: ByteRange = (0..<5) 123 | let expected2: ByteRange? = (3..<5) 124 | XCTAssertEqual(a2.trailingRange(in: b2), expected2) 125 | 126 | let a3: ByteRange = (0..<10) 127 | let b3: ByteRange = (0..<5) 128 | let expected3: ByteRange? = nil 129 | XCTAssertEqual(a3.trailingRange(in: b3), expected3) 130 | 131 | let a4: ByteRange = (0..<5) 132 | let b4: ByteRange = (0..<10) 133 | let expected4: ByteRange? = (5..<10) 134 | XCTAssertEqual(a4.trailingRange(in: b4), expected4) 135 | 136 | } 137 | 138 | func testItComputesCorrectTrailingRanges_DifferingStarts_SameEnds() { 139 | 140 | let a1: ByteRange = (5..<10) 141 | let b1: ByteRange = (0..<10) 142 | let expected1: ByteRange? = nil 143 | XCTAssertEqual(a1.trailingRange(in: b1), expected1) 144 | 145 | let a2: ByteRange = (0..<10) 146 | let b2: ByteRange = (5..<10) 147 | let expected2: ByteRange? = nil 148 | XCTAssertEqual(a2.trailingRange(in: b2), expected2) 149 | 150 | } 151 | 152 | func testItComputesCorrectTrailingRanges_DifferingStarts_DifferingEnds() { 153 | 154 | let a1: ByteRange = (0..<20) 155 | let b1: ByteRange = (5..<10) 156 | let expected1: ByteRange? = nil 157 | XCTAssertEqual(a1.trailingRange(in: b1), expected1) 158 | 159 | let a2: ByteRange = (5..<10) 160 | let b2: ByteRange = (0..<20) 161 | let expected2: ByteRange? = nil 162 | XCTAssertEqual(a2.trailingRange(in: b2), expected2) 163 | 164 | let a3: ByteRange = (10..<20) 165 | let b3: ByteRange = (5..<10) 166 | let expected3: ByteRange? = nil 167 | XCTAssertEqual(a3.trailingRange(in: b3), expected3) 168 | 169 | let a4: ByteRange = (5..<10) 170 | let b4: ByteRange = (10..<20) 171 | let expected4: ByteRange? = nil 172 | XCTAssertEqual(a4.trailingRange(in: b4), expected4) 173 | 174 | } 175 | 176 | func testItComputesCorrectTrailingRanges_OffByOnes() { 177 | 178 | let a1: ByteRange = (0..<2) 179 | let b1: ByteRange = (1..<2) 180 | let expected1: ByteRange? = nil 181 | XCTAssertEqual(a1.trailingRange(in: b1), expected1) 182 | 183 | let a2: ByteRange = (1..<2) 184 | let b2: ByteRange = (0..<2) 185 | let expected2: ByteRange? = nil 186 | XCTAssertEqual(a2.trailingRange(in: b2), expected2) 187 | 188 | let a3: ByteRange = (1..<2) 189 | let b3: ByteRange = (0..<1) 190 | let expected3: ByteRange? = nil 191 | XCTAssertEqual(a3.trailingRange(in: b3), expected3) 192 | 193 | let a4: ByteRange = (0..<1) 194 | let b4: ByteRange = (1..<2) 195 | let expected4: ByteRange? = nil 196 | XCTAssertEqual(a4.trailingRange(in: b4), expected4) 197 | 198 | } 199 | 200 | // MARK: Satisfaction 201 | 202 | func testItComputesSatisfaction_SameStarts_DifferingEnds() { 203 | 204 | let a1: ByteRange = (0..<5) 205 | let b1: ByteRange = (0..<5) 206 | XCTAssertTrue(a1.fullySatisfies(b1)) 207 | 208 | let a2: ByteRange = (0..<3) 209 | let b2: ByteRange = (0..<5) 210 | XCTAssertFalse(a2.fullySatisfies(b2)) 211 | 212 | let a3: ByteRange = (0..<10) 213 | let b3: ByteRange = (0..<5) 214 | XCTAssertTrue(a3.fullySatisfies(b3)) 215 | 216 | let a4: ByteRange = (0..<5) 217 | let b4: ByteRange = (0..<10) 218 | XCTAssertFalse(a4.fullySatisfies(b4)) 219 | 220 | } 221 | 222 | func testItComputesSatisfaction_DifferingStarts_SameEnds() { 223 | 224 | let a1: ByteRange = (5..<10) 225 | let b1: ByteRange = (0..<10) 226 | XCTAssertFalse(a1.fullySatisfies(b1)) 227 | 228 | let a2: ByteRange = (0..<10) 229 | let b2: ByteRange = (5..<10) 230 | XCTAssertTrue(a2.fullySatisfies(b2)) 231 | 232 | } 233 | 234 | func testItComputesSatisfaction_DifferingStarts_DifferingEnds() { 235 | 236 | let a1: ByteRange = (0..<20) 237 | let b1: ByteRange = (5..<10) 238 | XCTAssertTrue(a1.fullySatisfies(b1)) 239 | 240 | let a2: ByteRange = (5..<10) 241 | let b2: ByteRange = (0..<20) 242 | XCTAssertFalse(a2.fullySatisfies(b2)) 243 | 244 | let a3: ByteRange = (10..<20) 245 | let b3: ByteRange = (5..<10) 246 | XCTAssertFalse(a3.fullySatisfies(b3)) 247 | 248 | let a4: ByteRange = (5..<10) 249 | let b4: ByteRange = (10..<20) 250 | XCTAssertFalse(a4.fullySatisfies(b4)) 251 | 252 | } 253 | 254 | func testItComputesSatisfaction_OffByOnes() { 255 | 256 | let a1: ByteRange = (0..<2) 257 | let b1: ByteRange = (1..<2) 258 | XCTAssertTrue(a1.fullySatisfies(b1)) 259 | 260 | let a2: ByteRange = (1..<2) 261 | let b2: ByteRange = (0..<2) 262 | XCTAssertFalse(a2.fullySatisfies(b2)) 263 | 264 | let a3: ByteRange = (1..<2) 265 | let b3: ByteRange = (0..<1) 266 | XCTAssertFalse(a3.fullySatisfies(b3)) 267 | 268 | let a4: ByteRange = (0..<1) 269 | let b4: ByteRange = (1..<2) 270 | XCTAssertFalse(a4.fullySatisfies(b4)) 271 | 272 | } 273 | 274 | // MARK: Continuity 275 | 276 | func testItDetectsContinuity_SameStarts_DifferingEnds() { 277 | 278 | let a1: ByteRange = (0..<5) 279 | let b1: ByteRange = (0..<5) 280 | XCTAssertFalse(a1.isContiguousWith(b1)) 281 | 282 | let a2: ByteRange = (0..<3) 283 | let b2: ByteRange = (0..<5) 284 | XCTAssertFalse(a2.isContiguousWith(b2)) 285 | 286 | let a3: ByteRange = (0..<10) 287 | let b3: ByteRange = (0..<5) 288 | XCTAssertFalse(a3.isContiguousWith(b3)) 289 | 290 | let a4: ByteRange = (0..<5) 291 | let b4: ByteRange = (0..<10) 292 | XCTAssertFalse(a4.isContiguousWith(b4)) 293 | 294 | } 295 | 296 | func testItDetectsContinuity_DifferingStarts_SameEnds() { 297 | 298 | let a1: ByteRange = (5..<10) 299 | let b1: ByteRange = (0..<10) 300 | XCTAssertFalse(a1.isContiguousWith(b1)) 301 | 302 | let a2: ByteRange = (0..<10) 303 | let b2: ByteRange = (5..<10) 304 | XCTAssertFalse(a2.isContiguousWith(b2)) 305 | 306 | } 307 | 308 | func testItDetectsContinuity_DifferingStarts_DifferingEnds() { 309 | 310 | let a1: ByteRange = (0..<20) 311 | let b1: ByteRange = (5..<10) 312 | XCTAssertFalse(a1.isContiguousWith(b1)) 313 | 314 | let a2: ByteRange = (5..<10) 315 | let b2: ByteRange = (0..<20) 316 | XCTAssertFalse(a2.isContiguousWith(b2)) 317 | 318 | let a3: ByteRange = (10..<20) 319 | let b3: ByteRange = (5..<10) 320 | XCTAssertTrue(a3.isContiguousWith(b3)) 321 | 322 | let a4: ByteRange = (5..<10) 323 | let b4: ByteRange = (10..<20) 324 | XCTAssertTrue(a4.isContiguousWith(b4)) 325 | 326 | } 327 | 328 | func testItDetectsContinuity_OffByOnes() { 329 | 330 | let a1: ByteRange = (0..<2) 331 | let b1: ByteRange = (1..<2) 332 | XCTAssertFalse(a1.isContiguousWith(b1)) 333 | 334 | let a2: ByteRange = (1..<2) 335 | let b2: ByteRange = (0..<2) 336 | XCTAssertFalse(a2.isContiguousWith(b2)) 337 | 338 | let a3: ByteRange = (1..<2) 339 | let b3: ByteRange = (0..<1) 340 | XCTAssertTrue(a3.isContiguousWith(b3)) 341 | 342 | let a4: ByteRange = (0..<1) 343 | let b4: ByteRange = (1..<2) 344 | XCTAssertTrue(a4.isContiguousWith(b4)) 345 | 346 | } 347 | 348 | // MARK: Combination 349 | 350 | func testItCombinesArraysOfRanges_nonOverlapping() { 351 | 352 | var input: [ByteRange]; 353 | var expected: [ByteRange] 354 | var combination: [ByteRange] 355 | 356 | input = [] 357 | expected = [] 358 | combination = combine(input) 359 | XCTAssertEqual(combination, expected) 360 | 361 | input = [(0..<2)] 362 | expected = [(0..<2)] 363 | combination = combine(input) 364 | XCTAssertEqual(combination, expected) 365 | 366 | input = [(0..<2), (8..<10)] 367 | expected = [(0..<2), (8..<10)] 368 | combination = combine(input) 369 | XCTAssertEqual(combination, expected) 370 | 371 | input = [(0..<2), (2..<10)] 372 | expected = [(0..<10)] 373 | combination = combine(input) 374 | XCTAssertEqual(combination, expected) 375 | 376 | input = [(0..<4), (8..<10), (3..<9)] 377 | expected = [(0..<10)] 378 | combination = combine(input) 379 | XCTAssertEqual(combination, expected) 380 | 381 | input = [(0..<4), (10..<30), (20..<40), (90..<100)] 382 | expected = [(0..<4), (10..<40), (90..<100)] 383 | combination = combine(input) 384 | XCTAssertEqual(combination, expected) 385 | 386 | input = [(20..<40), (10..<30), (90..<100), (0..<4)] 387 | expected = [(0..<4), (10..<40), (90..<100)] 388 | combination = combine(input) 389 | XCTAssertEqual(combination, expected) 390 | 391 | input = [(0..<1), (2..<3)] 392 | expected = [(0..<1), (2..<3)] 393 | combination = combine(input) 394 | XCTAssertEqual(combination, expected) 395 | 396 | input = [(5..<10), (3..<11)] 397 | expected = [(3..<11)] 398 | combination = combine(input) 399 | XCTAssertEqual(combination, expected) 400 | 401 | } 402 | 403 | func testItCombinesArraysOfRanges_overlapping() { 404 | 405 | var input: [ByteRange]; 406 | var expected: [ByteRange] 407 | var combination: [ByteRange] 408 | 409 | input = [(0..<10), (0..<10), (20..<40), (20..<40)] 410 | expected = [(0..<10), (20..<40)] 411 | combination = combine(input) 412 | XCTAssertEqual(combination, expected) 413 | 414 | } 415 | 416 | // MARK: Relative Position 417 | 418 | func testItYieldsCorrectRelativePositions() { 419 | let range: ByteRange = (10..<20) 420 | XCTAssertEqual(range.relativePosition(of: 0), ByteRangeIndexPosition.before) 421 | XCTAssertEqual(range.relativePosition(of: 9), ByteRangeIndexPosition.before) 422 | XCTAssertEqual(range.relativePosition(of: 10), ByteRangeIndexPosition.inside) 423 | XCTAssertEqual(range.relativePosition(of: 11), ByteRangeIndexPosition.inside) 424 | XCTAssertEqual(range.relativePosition(of: 19), ByteRangeIndexPosition.inside) 425 | XCTAssertEqual(range.relativePosition(of: 20), ByteRangeIndexPosition.after) 426 | XCTAssertEqual(range.relativePosition(of: 21), ByteRangeIndexPosition.after) 427 | } 428 | 429 | } 430 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/DataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataTests.swift 3 | // SodesAudioTests 4 | // 5 | // Created by Jared Sinclair on 8/19/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesAudio 11 | 12 | class DataTests: XCTestCase { 13 | 14 | func test_itReturnsSubdataForValidResponse_Variant1() { 15 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9]) 16 | let range: ByteRange = (0..<10) 17 | let subdata = data.byteRangeResponseSubdata(in: range) 18 | XCTAssertEqual(subdata, data) 19 | } 20 | 21 | func test_itReturnsSubdataForValidResponse_Variant2() { 22 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9]) 23 | let range: ByteRange = (100..<110) 24 | let subdata = data.byteRangeResponseSubdata(in: range) 25 | XCTAssertEqual(subdata, data) 26 | } 27 | 28 | func test_itReturnsNilForAnInvalidResponse_Variant1() { 29 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9]) 30 | let range: ByteRange = (0..<100) 31 | let subdata = data.byteRangeResponseSubdata(in: range) 32 | XCTAssertNil(subdata) 33 | } 34 | 35 | func test_itReturnsNilForAnInvalidResponse_Variant2() { 36 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9]) 37 | let range: ByteRange = (100..<200) 38 | let subdata = data.byteRangeResponseSubdata(in: range) 39 | XCTAssertNil(subdata) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/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 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/ResourceLoaderSubrequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceLoaderSubrequestTests.swift 3 | // SodesAudioTests 4 | // 5 | // Created by Jared Sinclair on 8/6/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesAudio 11 | 12 | class ResourceLoaderSubrequestTests: XCTestCase { 13 | 14 | func testItProducesOneBigNetworkRequestWhenNoRangesAreFound() { 15 | let requestedRange: ByteRange = (10..<600) 16 | let subrequests = ResourceLoaderSubrequest.subrequests( 17 | requestedRange: requestedRange, 18 | scratchFileRanges: [] 19 | ) 20 | XCTAssertEqual(subrequests.count, 1) 21 | XCTAssertEqual(subrequests[0], ResourceLoaderSubrequest(source: .network, range: requestedRange)) 22 | } 23 | 24 | func testItProducesOneBigScratchFileRequestWhenTheFullRangeIsSatisfied() { 25 | let requestedRange: ByteRange = (10..<600) 26 | let subrequests = ResourceLoaderSubrequest.subrequests( 27 | requestedRange: requestedRange, 28 | scratchFileRanges: [(0..<1000)] 29 | ) 30 | XCTAssertEqual(subrequests.count, 1) 31 | XCTAssertEqual(subrequests[0], ResourceLoaderSubrequest(source: .scratchFile, range: requestedRange)) 32 | } 33 | 34 | func testItProducesCorrectSubrequests_variant1() { 35 | let requestedRange: ByteRange = (10..<600) 36 | let subrequests = ResourceLoaderSubrequest.subrequests( 37 | requestedRange: requestedRange, 38 | scratchFileRanges: [(100..<200), (300..<400), (500..<550)] 39 | ) 40 | XCTAssertEqual(subrequests.count, 7) 41 | let expected = [ 42 | ResourceLoaderSubrequest(source: .network, range: (010..<100)), 43 | ResourceLoaderSubrequest(source: .scratchFile, range: (100..<200)), 44 | ResourceLoaderSubrequest(source: .network, range: (200..<300)), 45 | ResourceLoaderSubrequest(source: .scratchFile, range: (300..<400)), 46 | ResourceLoaderSubrequest(source: .network, range: (400..<500)), 47 | ResourceLoaderSubrequest(source: .scratchFile, range: (500..<550)), 48 | ResourceLoaderSubrequest(source: .network, range: (550..<600)), 49 | ] 50 | XCTAssertEqual(subrequests, expected) 51 | } 52 | 53 | func testItProducesCorrectSubrequests_variant2() { 54 | let requestedRange: ByteRange = (10..<600) 55 | let subrequests = ResourceLoaderSubrequest.subrequests( 56 | requestedRange: requestedRange, 57 | scratchFileRanges: [(100..<200), (300..<400), (500..<600)] 58 | ) 59 | XCTAssertEqual(subrequests.count, 6) 60 | let expected = [ 61 | ResourceLoaderSubrequest(source: .network, range: (010..<100)), 62 | ResourceLoaderSubrequest(source: .scratchFile, range: (100..<200)), 63 | ResourceLoaderSubrequest(source: .network, range: (200..<300)), 64 | ResourceLoaderSubrequest(source: .scratchFile, range: (300..<400)), 65 | ResourceLoaderSubrequest(source: .network, range: (400..<500)), 66 | ResourceLoaderSubrequest(source: .scratchFile, range: (500..<600)), 67 | ] 68 | XCTAssertEqual(subrequests, expected) 69 | } 70 | 71 | func testItProducesCorrectSubrequests_variant3() { 72 | let requestedRange: ByteRange = (0..<3) 73 | let subrequests = ResourceLoaderSubrequest.subrequests( 74 | requestedRange: requestedRange, 75 | scratchFileRanges: [(1..<2)] 76 | ) 77 | XCTAssertEqual(subrequests.count, 3) 78 | let expected = [ 79 | ResourceLoaderSubrequest(source: .network, range: (0..<1)), 80 | ResourceLoaderSubrequest(source: .scratchFile, range: (1..<2)), 81 | ResourceLoaderSubrequest(source: .network, range: (2..<3)), 82 | ] 83 | XCTAssertEqual(subrequests, expected) 84 | } 85 | 86 | func testItProducesCorrectSubrequests_variant4() { 87 | let requestedRange: ByteRange = (1..<2) 88 | let subrequests = ResourceLoaderSubrequest.subrequests( 89 | requestedRange: requestedRange, 90 | scratchFileRanges: [(0..<3)] 91 | ) 92 | XCTAssertEqual(subrequests.count, 1) 93 | let expected = [ 94 | ResourceLoaderSubrequest(source: .scratchFile, range: (1..<2)), 95 | ] 96 | XCTAssertEqual(subrequests, expected) 97 | } 98 | 99 | func testItProducesCorrectSubrequests_variant5() { 100 | let requestedRange: ByteRange = (0..<10) 101 | let subrequests = ResourceLoaderSubrequest.subrequests( 102 | requestedRange: requestedRange, 103 | scratchFileRanges: [(0..<5)] 104 | ) 105 | XCTAssertEqual(subrequests.count, 2) 106 | let expected = [ 107 | ResourceLoaderSubrequest(source: .scratchFile, range: (0..<5)), 108 | ResourceLoaderSubrequest(source: .network, range: (5..<10)), 109 | ] 110 | XCTAssertEqual(subrequests, expected) 111 | } 112 | 113 | func testItProducesCorrectSubrequests_variant6() { 114 | let requestedRange: ByteRange = (2..<10) 115 | let subrequests = ResourceLoaderSubrequest.subrequests( 116 | requestedRange: requestedRange, 117 | scratchFileRanges: [(0..<2), (2..<5), (4..<10)] 118 | ) 119 | XCTAssertEqual(subrequests.count, 2) 120 | let expected = [ 121 | ResourceLoaderSubrequest(source: .scratchFile, range: (2..<5)), 122 | ResourceLoaderSubrequest(source: .scratchFile, range: (5..<10)), 123 | ] 124 | XCTAssertEqual(subrequests, expected) 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/URLRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequestTests.swift 3 | // SodesAudioTests 4 | // 5 | // Created by Jared Sinclair on 8/19/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesAudio 11 | 12 | class URLRequestTests: XCTestCase { 13 | 14 | func test_itCreatesADataRequest() { 15 | let url = URL(string: "http://example.com/audio.mp3")! 16 | let range: ByteRange = (100..<200) 17 | let request = URLRequest.dataRequest(from: url, for: range) 18 | XCTAssertEqual(request.cachePolicy, .reloadIgnoringLocalCacheData) 19 | XCTAssertEqual(request.url, url) 20 | XCTAssertEqual(request.allHTTPHeaderFields!["Range"], "bytes=100-199") 21 | } 22 | 23 | func test_itExtractsRequestedByteRange() { 24 | let url = URL(string: "http://example.com/audio.mp3")! 25 | let range: ByteRange = (100..<200) 26 | let request = URLRequest.dataRequest(from: url, for: range) 27 | XCTAssertEqual(request.byteRange, range) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sodes/SodesAudioTests/URLResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLResponseTests.swift 3 | // SodesAudioTests 4 | // 5 | // Created by Jared Sinclair on 8/20/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesAudio 11 | 12 | class URLResponseTests: XCTestCase { 13 | 14 | func test_itExtractsResponseRangeAndTotalExpectedContentLength() { 15 | let url = URL(string: "http://example.com/audio.mp3")! 16 | let response = HTTPURLResponse( 17 | url: url, 18 | statusCode: 206, 19 | httpVersion: nil, 20 | headerFields: [ 21 | "Content-Range": "bytes 100-199/444444" 22 | ] 23 | ) 24 | let expectedRange: ByteRange = (100..<200) 25 | XCTAssertEqual(response?.sodes_responseRange, expectedRange) 26 | XCTAssertEqual(response?.sodes_expectedContentLength, 444444) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sodes/SodesExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SodesExample 4 | // 5 | // Created by Jared Sinclair on 9/2/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sodes/SodesExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Sodes/SodesExample/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 | -------------------------------------------------------------------------------- /Sodes/SodesExample/Base.lproj/Main.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 | 38 | 43 | 44 | 45 | 54 | 63 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /Sodes/SodesExample/EpisodeDuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EpisodeDuration.swift 3 | // Sodes 4 | // 5 | // Created by Jared Sinclair on 8/28/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public struct EpisodeDurationParsing { 12 | 13 | private static let lock = NSLock() 14 | private static let calendar = Calendar(identifier: .gregorian) 15 | 16 | private static let hourMinuteSecondFormatter: DateFormatter = { 17 | let formatter = DateFormatter() 18 | formatter.dateFormat = "HH:mm:ss" 19 | return formatter 20 | }() 21 | 22 | private static let minuteSecondFormatter: DateFormatter = { 23 | let formatter = DateFormatter() 24 | formatter.dateFormat = "mm:ss" 25 | return formatter 26 | }() 27 | 28 | public static func duration(from string: String) -> TimeInterval? { 29 | 30 | let comps = string.components(separatedBy: ":").filter{!$0.isEmpty} 31 | 32 | guard !comps.isEmpty else {return nil} 33 | 34 | let duration: TimeInterval 35 | 36 | if comps.count == 1 && string.range(of: ":") == nil { 37 | duration = comps[0].toSeconds(constrainToClockRange: false) 38 | } 39 | else if comps.count == 2 { 40 | guard comps[0].characters.count == 2 else {return nil} 41 | guard comps[1].characters.count == 2 else {return nil} 42 | duration = 43 | comps[0].toMinutes() * 60 44 | + comps[1].toSeconds(constrainToClockRange: true) 45 | } 46 | else if comps.count == 3 { 47 | guard comps[1].characters.count == 2 else {return nil} 48 | guard comps[2].characters.count == 2 else {return nil} 49 | duration = 50 | comps[0].toHours() * 3600 51 | + comps[1].toMinutes() * 60 52 | + comps[2].toSeconds(constrainToClockRange: true) 53 | } 54 | else { 55 | duration = 0 56 | } 57 | 58 | return (duration > 0) ? duration : nil 59 | 60 | } 61 | 62 | public static func string(from duration: TimeInterval) -> String { 63 | 64 | guard duration > 0 else {return "00:00"} 65 | guard duration <= (23*3600 + 59*60 + 59) else {return "23:59:59"} 66 | 67 | let hours = floor(duration / 3600) 68 | let minutesAndSeconds = duration.truncatingRemainder(dividingBy: 3600) 69 | let minutes = floor(minutesAndSeconds / 60) 70 | let seconds = floor(minutesAndSeconds.truncatingRemainder(dividingBy: 60)) 71 | 72 | var comps = DateComponents() 73 | comps.calendar = calendar 74 | comps.day = 1 75 | comps.month = 1 76 | comps.year = 2016 77 | comps.hour = Int(hours) 78 | comps.minute = Int(minutes) 79 | comps.second = Int(seconds) 80 | 81 | guard let date = comps.date else {return "00:00"} 82 | 83 | if hours > 0 { 84 | lock.lock() 85 | let result = hourMinuteSecondFormatter.string(from: date) 86 | lock.unlock() 87 | return result 88 | } else { 89 | lock.lock() 90 | let result = minuteSecondFormatter.string(from: date) 91 | lock.unlock() 92 | return result 93 | } 94 | 95 | } 96 | 97 | private init() {} 98 | 99 | } 100 | 101 | private extension String { 102 | 103 | func toSeconds(constrainToClockRange: Bool) -> TimeInterval { 104 | guard let possible = TimeInterval(self) else {return 0} 105 | if (constrainToClockRange) { 106 | return (0 <= possible && possible <= 59) ? possible : 0 107 | } else { 108 | return possible 109 | } 110 | } 111 | 112 | func toMinutes() -> TimeInterval { 113 | guard let possible = TimeInterval(self) else {return 0} 114 | return (0 <= possible && possible <= 59) ? possible : 0 115 | } 116 | 117 | func toHours() -> TimeInterval { 118 | return TimeInterval(self) ?? 0 119 | } 120 | 121 | } 122 | 123 | private func *(lhs: TimeInterval?, rhs: TimeInterval) -> TimeInterval { 124 | return (lhs ?? 0) * rhs 125 | } 126 | -------------------------------------------------------------------------------- /Sodes/SodesExample/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Sodes/SodesExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SodesExample 4 | // 5 | // Created by Jared Sinclair on 9/2/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | import SodesAudio 11 | import MediaPlayer 12 | 13 | struct TestSource: PlaybackSource { 14 | let uniqueId: String = "abcxyz" 15 | var artistId: String = "123456" 16 | var remoteUrl: URL = URL(string: "http://content.blubrry.com/exponent/exponent86.mp3")! 17 | var title: String? = "Track Title" 18 | var albumTitle: String? = "Album Title" 19 | var artist: String? = "Artist" 20 | var artworkUrl: URL? = URL(string: "http://exponent.fm/wp-content/uploads/2014/02/cropped-Exponent-header.png") 21 | var mediaType: MPMediaType = .podcast 22 | var expectedLengthInBytes: Int64? = nil 23 | } 24 | 25 | class ViewController: UIViewController { 26 | 27 | @IBOutlet private var playPauseButton: UIButton! 28 | @IBOutlet private var backButton: UIButton! 29 | @IBOutlet private var forwardButton: UIButton! 30 | @IBOutlet private var progressBar: UIProgressView! 31 | @IBOutlet private var elapsedTimeLabel: UILabel! 32 | @IBOutlet private var remainingTimeLabel: UILabel! 33 | @IBOutlet private var activityIndicator: UIActivityIndicatorView! 34 | @IBOutlet private var byteRangeTextView: UITextView! 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | let source = TestSource() 40 | PlaybackController.sharedController.prepare(source, startTime: 0, playWhenReady: true) 41 | 42 | let center = NotificationCenter.default 43 | 44 | center.addObserver(forName: PlaybackControllerNotification.DidUpdateElapsedTime.name, object: nil, queue: .main) { (note) in 45 | let controller = PlaybackController.sharedController 46 | guard let duration = controller.duration else {return} 47 | let elapsed = floor(controller.elapsedTime) 48 | self.elapsedTimeLabel.text = EpisodeDurationParsing.string(from: elapsed) 49 | let remaining = floor(duration - elapsed) 50 | self.remainingTimeLabel.text = EpisodeDurationParsing.string(from: remaining) 51 | self.progressBar.progress = Float(elapsed / duration) 52 | } 53 | 54 | center.addObserver(forName: PlaybackControllerNotification.DidUpdateStatus.name, object: nil, queue: .main) { (note) in 55 | switch PlaybackController.sharedController.status { 56 | case .buffering, .preparing(_,_): 57 | self.activityIndicator.startAnimating() 58 | default: 59 | self.activityIndicator.stopAnimating() 60 | } 61 | } 62 | 63 | center.addObserver(forName: PlaybackControllerNotification.DidUpdateLoadedByteRanges.name, object: nil, queue: .main) { (note) in 64 | let key = PlaybackControllerNotification.ByteRangesKey 65 | if let ranges = note.userInfo?[key] as? [ByteRange] { 66 | self.byteRangeTextView.text = "\(ranges)" 67 | } 68 | } 69 | 70 | } 71 | 72 | @IBAction func togglePlayPause(sender: AnyObject?) { 73 | PlaybackController.sharedController.togglePlayPause() 74 | } 75 | 76 | @IBAction func back(sender: AnyObject?) { 77 | PlaybackController.sharedController.skipBackward() 78 | } 79 | 80 | @IBAction func forward(sender: AnyObject?) { 81 | PlaybackController.sharedController.skipForward() 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/AsyncBlockOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockOperation.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class AsyncBlockOperation: WorkOperation { 12 | 13 | public typealias FinishHandler = () -> Void 14 | public typealias WorkBlock = (@escaping FinishHandler) -> Void 15 | 16 | private let workBlock: WorkBlock 17 | 18 | public init(work: @escaping WorkBlock) { 19 | self.workBlock = work 20 | super.init{} 21 | } 22 | 23 | public init(work: @escaping WorkBlock, completion: @escaping () -> Void) { 24 | self.workBlock = work 25 | super.init(completion: completion) 26 | } 27 | 28 | override public func work(_ finish: @escaping FinishHandler) { 29 | workBlock(finish) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/DelegatedHTTPOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DelegatedHTTPOperation.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 8/6/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol HTTPOperationDataDelegate: class { 12 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveResponse response: HTTPURLResponse) 13 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveData data: Data) 14 | } 15 | 16 | public class DelegatedHTTPOperation: WorkOperation { 17 | 18 | public enum Result { 19 | case success(response: HTTPURLResponse, bytesExpected: Int64, bytesReceived: Int64) 20 | case error(HTTPURLResponse?, Error?) 21 | } 22 | 23 | public let request: URLRequest 24 | 25 | fileprivate let delegateQueue: OperationQueue 26 | fileprivate weak var dataDelegate: HTTPOperationDataDelegate? 27 | fileprivate let internals: DelegatedHTTPOperationInternals 28 | fileprivate var session: URLSession! 29 | fileprivate var task: URLSessionDataTask! 30 | 31 | fileprivate var finish: (() -> Void)? 32 | fileprivate var response: HTTPURLResponse? = nil 33 | fileprivate var bytesExpected: Int64 = 0 34 | fileprivate var bytesReceived: Int64 = 0 35 | 36 | public init(url: URL, configuration: URLSessionConfiguration = .default, dataDelegate: HTTPOperationDataDelegate, delegateQueue: OperationQueue, completion: @escaping (Result) -> Void) { 37 | assert(delegateQueue.maxConcurrentOperationCount == 1, "DelegatedHTTPOperation's delegate queue must be a serial queue.") 38 | let internals = DelegatedHTTPOperationInternals() 39 | self.internals = internals 40 | self.delegateQueue = delegateQueue 41 | self.dataDelegate = dataDelegate 42 | self.request = URLRequest(url: url) 43 | super.init { completion(internals.result) } 44 | self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue) 45 | self.task = session.dataTask(with: request) 46 | } 47 | 48 | public init(request: URLRequest, configuration: URLSessionConfiguration = .default, dataDelegate: HTTPOperationDataDelegate, delegateQueue: OperationQueue, completion: @escaping (Result) -> Void) { 49 | assert(delegateQueue.maxConcurrentOperationCount == 1, "DelegatedHTTPOperation's delegate queue must be a serial queue.") 50 | let internals = DelegatedHTTPOperationInternals() 51 | self.internals = internals 52 | self.delegateQueue = delegateQueue 53 | self.dataDelegate = dataDelegate 54 | self.request = request 55 | super.init { completion(internals.result) } 56 | self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue) 57 | self.task = session.dataTask(with: request) 58 | } 59 | 60 | override public func work(_ finish: @escaping () -> Void) { 61 | self.finish = finish 62 | task.resume() 63 | } 64 | 65 | override public func cancel() { 66 | task.cancel() 67 | super.cancel() 68 | } 69 | 70 | fileprivate func invokeFinishHandler() { 71 | delegateQueue.addOperation { [weak self] in 72 | guard let this = self else {return} 73 | guard !this.isCancelled else {return} 74 | let handler = this.finish 75 | this.finish = nil 76 | handler?() 77 | } 78 | } 79 | 80 | } 81 | 82 | extension DelegatedHTTPOperation: URLSessionDataDelegate { 83 | 84 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 85 | 86 | guard !isCancelled else {return} 87 | 88 | // TODO: [MEDIUM] Consider how to handle redirects (POST vs GET e.g.) 89 | guard let httpResponse = response as? HTTPURLResponse, 200 <= httpResponse.statusCode, httpResponse.statusCode <= 299 else 90 | { 91 | SodesLog("Invalid response received: \(response)") 92 | completionHandler(.cancel) 93 | internals.result = .error((response as? HTTPURLResponse), nil) 94 | invokeFinishHandler() 95 | return 96 | } 97 | 98 | self.response = httpResponse 99 | self.bytesExpected = httpResponse.contentLength 100 | SodesLog("self.bytesExpected: \(bytesExpected)\nresponse: \(httpResponse)") 101 | 102 | delegateQueue.addOperation { [weak self] in 103 | guard let this = self else {return} 104 | guard !this.isCancelled else {return} 105 | this.dataDelegate?.delegatedHTTPOperation(this, didReceiveResponse: httpResponse) 106 | } 107 | 108 | completionHandler(.allow) 109 | } 110 | 111 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 112 | guard !isCancelled else {return} 113 | bytesReceived += Int64(data.count) 114 | delegateQueue.addOperation { [weak self] in 115 | guard let this = self else {return} 116 | guard !this.isCancelled else {return} 117 | this.dataDelegate?.delegatedHTTPOperation(this, didReceiveData: data) 118 | } 119 | } 120 | 121 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 122 | guard !isCancelled else {return} 123 | if let response = self.response, error == nil { 124 | internals.result = .success( 125 | response: response, 126 | bytesExpected: bytesExpected, 127 | bytesReceived: bytesReceived 128 | ) 129 | } else { 130 | internals.result = .error(response, error) 131 | } 132 | invokeFinishHandler() 133 | } 134 | 135 | } 136 | 137 | fileprivate class DelegatedHTTPOperationInternals { 138 | var result: DelegatedHTTPOperation.Result = .error(nil, nil) 139 | } 140 | 141 | fileprivate extension HTTPURLResponse { 142 | var contentLength: Int64 { 143 | if let s = allHeaderFields["Content-Length"] as? String { 144 | return Int64(s) ?? 0 145 | } 146 | return allHeaderFields["Content-Length"] as? Int64 ?? 0 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/DownloadOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadOperation.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class DownloadOperation: WorkOperation { 12 | 13 | public enum Result { 14 | case tempUrl(URL, HTTPURLResponse) 15 | case error(HTTPURLResponse?, Error?) 16 | } 17 | 18 | private let internals: DownloadOperationInternals 19 | 20 | public init(url: URL, session: URLSession, completion: @escaping (Result) -> Void) { 21 | let internals = DownloadOperationInternals( 22 | request: URLRequest(url: url), 23 | session: session 24 | ) 25 | self.internals = internals 26 | super.init { 27 | completion(internals.result) 28 | } 29 | } 30 | 31 | public init(request: URLRequest, session: URLSession, completion: @escaping (Result) -> Void) { 32 | let internals = DownloadOperationInternals( 33 | request: request, 34 | session: session 35 | ) 36 | self.internals = internals 37 | super.init { 38 | completion(internals.result) 39 | } 40 | } 41 | 42 | override public func work(_ finish: @escaping () -> Void) { 43 | internals.task = internals.session.downloadTask(with: internals.request) { (tempUrl, response, error) in 44 | guard !self.isCancelled else {return} 45 | // TODO: [MEDIUM] Consider how to handle redirects (POST vs GET e.g.) 46 | guard 47 | let r = response as? HTTPURLResponse, 48 | let tempUrl = tempUrl, 49 | 200 <= r.statusCode && r.statusCode <= 299 else 50 | { 51 | self.internals.result = .error((response as? HTTPURLResponse), error) 52 | finish() 53 | return 54 | } 55 | self.internals.result = .tempUrl(tempUrl, r) 56 | finish() 57 | } 58 | internals.task?.resume() 59 | } 60 | 61 | override public func cancel() { 62 | internals.task?.cancel() 63 | super.cancel() 64 | } 65 | 66 | } 67 | 68 | private class DownloadOperationInternals { 69 | 70 | let request: URLRequest 71 | let session: URLSession 72 | var task: URLSessionDownloadTask? 73 | var result: DownloadOperation.Result = .error(nil, nil) 74 | 75 | init(request: URLRequest, session: URLSession) { 76 | self.request = request 77 | self.session = session 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // Sodes 4 | // 5 | // Created by Jared Sinclair on 8/30/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public func description(of optionalError: Error?) -> String { 12 | if let error = optionalError { 13 | let type = type(of: error) 14 | if type == NSError.self { 15 | let nsError = error as NSError 16 | return nsError.domain + "_\(nsError.code)" 17 | } else { 18 | return String(describing: type) + "." + String(describing: error) 19 | } 20 | } else { 21 | return "nil" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManagement.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public extension FileManager { 12 | 13 | public func cachesDirectory() -> URL? { 14 | let directories = urls( 15 | for: .cachesDirectory, 16 | in: .userDomainMask 17 | ) 18 | return directories.first 19 | } 20 | 21 | public func documentsDirectory() -> URL? { 22 | let directories = urls( 23 | for: .documentDirectory, 24 | in: .userDomainMask 25 | ) 26 | return directories.first 27 | } 28 | 29 | public func createDirectoryAt(_ url: URL) -> Bool { 30 | do { 31 | try createDirectory( 32 | at: url, 33 | withIntermediateDirectories: true, 34 | attributes: nil 35 | ) 36 | } 37 | catch { 38 | return false 39 | } 40 | return true 41 | } 42 | 43 | public func createSubdirectory(_ name: String, atUrl url: URL) -> Bool { 44 | let subdirectoryUrl = url.appendingPathComponent(name, isDirectory: true) 45 | return self.createDirectoryAt(subdirectoryUrl) 46 | } 47 | 48 | public func removeDirectory(_ directory: URL) -> Bool { 49 | do { 50 | try removeItem(at: directory) 51 | } 52 | catch { 53 | return false 54 | } 55 | return true 56 | } 57 | 58 | public func removeFile(at url: URL) -> Bool { 59 | do { 60 | try removeItem(at: url) 61 | } 62 | catch { 63 | return false 64 | } 65 | return true 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/GCD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCD.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public func onMainQueue(_ block: @escaping () -> Void) { 12 | DispatchQueue.main.async(execute: block) 13 | } 14 | 15 | public func onMainQueueSyncIfPossible(_ block: @escaping () -> Void) { 16 | if OperationQueue.current === OperationQueue.main { 17 | block() 18 | } else { 19 | DispatchQueue.main.async(execute: block) 20 | } 21 | } 22 | 23 | public func onGlobalQueue(_ block: @escaping () -> Void) { 24 | DispatchQueue.global().async(execute: block) 25 | } 26 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/HTTPOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPOperation.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class HTTPOperation: WorkOperation { 12 | 13 | public enum Result { 14 | case data(Data, HTTPURLResponse) 15 | case error(HTTPURLResponse?, Error?) 16 | } 17 | 18 | private let internals: HTTPOperationInternals 19 | 20 | public init(url: URL, session: URLSession, completion: @escaping (Result) -> Void) { 21 | let internals = HTTPOperationInternals( 22 | request: URLRequest(url: url), 23 | session: session 24 | ) 25 | self.internals = internals 26 | super.init { 27 | completion(internals.result) 28 | } 29 | } 30 | 31 | public init(request: URLRequest, session: URLSession, completion: @escaping (Result) -> Void) { 32 | let internals = HTTPOperationInternals( 33 | request: request, 34 | session: session 35 | ) 36 | self.internals = internals 37 | super.init { 38 | completion(internals.result) 39 | } 40 | } 41 | 42 | override public func work(_ finish: @escaping () -> Void) { 43 | internals.task = internals.session.dataTask(with: internals.request) { (data, response, error) in 44 | guard !self.isCancelled else {return} 45 | // TODO: [MEDIUM] Consider how to handle redirects (POST vs GET e.g.) 46 | guard let r = response as? HTTPURLResponse, let data = data, 200 <= r.statusCode, r.statusCode <= 299 else 47 | { 48 | self.internals.result = .error((response as? HTTPURLResponse), error) 49 | finish() 50 | return 51 | } 52 | self.internals.result = .data(data, r) 53 | finish() 54 | } 55 | internals.task?.resume() 56 | } 57 | 58 | override public func cancel() { 59 | internals.task?.cancel() 60 | super.cancel() 61 | } 62 | 63 | } 64 | 65 | private class HTTPOperationInternals { 66 | 67 | let request: URLRequest 68 | let session: URLSession 69 | var task: URLSessionDataTask? 70 | var result: HTTPOperation.Result = .error(nil, nil) 71 | 72 | init(request: URLRequest, session: URLSession) { 73 | self.request = request 74 | self.session = session 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/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 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/NSDateFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateFormatter.swift 3 | // SodesCore 4 | // 5 | // Created by Jared Sinclair on 7/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class RFC822Formatter { 12 | 13 | public static let sharedFormatter = RFC822Formatter() 14 | 15 | private let formatterWithWeekdays: DateFormatter 16 | private let formatterWithoutWeekdays: DateFormatter 17 | private let lock = NSLock() 18 | 19 | public init() { 20 | formatterWithWeekdays = DateFormatter.RFC822Formatter(includeWeekdays: true) 21 | formatterWithoutWeekdays = DateFormatter.RFC822Formatter(includeWeekdays: false) 22 | } 23 | 24 | public func string(from date: Date) -> String? { 25 | lock.lock() 26 | let withWeekdays = formatterWithWeekdays.string(from: date) 27 | lock.unlock() 28 | return withWeekdays 29 | } 30 | 31 | public func date(from string: String) -> Date? { 32 | 33 | // TODO: [MEDIUM] Ensure that we don't need a more efficient means of knowing 34 | // which formatter to try first (or not at all), etc. 35 | 36 | lock.lock() 37 | let withWeekdays = formatterWithWeekdays.date(from: string) 38 | lock.unlock() 39 | if let with = withWeekdays { 40 | return with 41 | } 42 | 43 | lock.lock() 44 | let withoutWeekdays = formatterWithoutWeekdays.date(from: string) 45 | lock.unlock() 46 | return withoutWeekdays 47 | 48 | } 49 | 50 | } 51 | 52 | private extension DateFormatter { 53 | 54 | static func RFC822Formatter(includeWeekdays include: Bool) -> DateFormatter { 55 | let formatter = DateFormatter() 56 | formatter.locale = Locale(identifier: "en_US_POSIX") 57 | if include { 58 | formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z" 59 | } else { 60 | formatter.dateFormat = "dd MMM yyyy HH:mm:ss z" 61 | } 62 | return formatter 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/SodesLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SodesLog.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/25/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public func SodesLog(_ input: Any = "", file: String = #file, function: String = #function, line: Int = #line) { 12 | #if DEBUG 13 | print("\n\(NSDate())\n\(file):\n\(function)() Line \(line)\n\(input)\n\n") 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 8/11/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public class Throttle { 12 | 13 | public typealias Action = () -> Void 14 | 15 | fileprivate let serialQueue: OperationQueue 16 | fileprivate var pendingAction: Action? 17 | fileprivate var hasTakenActionAtLeastOnce = false 18 | fileprivate var timer: Timer? 19 | fileprivate var timerTarget: TimerTarget! { 20 | didSet { assert(oldValue == nil) } 21 | } 22 | 23 | public init(minimumInterval: TimeInterval, qualityOfService: QualityOfService) { 24 | serialQueue = { 25 | let queue = OperationQueue() 26 | queue.qualityOfService = qualityOfService 27 | queue.maxConcurrentOperationCount = 1 28 | return queue 29 | }() 30 | timerTarget = TimerTarget(delegate: self) 31 | timer = { 32 | let timer = Timer( 33 | timeInterval: minimumInterval, 34 | target: timerTarget, 35 | selector: #selector(TimerTarget.timerFired), 36 | userInfo: nil, 37 | repeats: true 38 | ) 39 | timer.tolerance = minimumInterval * 0.5 40 | RunLoop.main.add(timer, forMode: .commonModes) 41 | return timer 42 | }() 43 | } 44 | 45 | deinit { 46 | timer?.invalidate() 47 | timer = nil 48 | } 49 | 50 | public func enqueue(immediately: Bool = false, action: @escaping Action) { 51 | let op = BlockOperation { [weak self] in 52 | guard let this = self else {return} 53 | if !this.hasTakenActionAtLeastOnce || immediately { 54 | this.hasTakenActionAtLeastOnce = true 55 | this.pendingAction = nil 56 | action() 57 | } else { 58 | this.pendingAction = action 59 | } 60 | } 61 | op.queuePriority = immediately ? .veryHigh : .normal 62 | serialQueue.addOperation(op) 63 | } 64 | 65 | } 66 | 67 | extension Throttle: TimerTargetDelegate { 68 | 69 | fileprivate func timerFired(for: TimerTarget) { 70 | serialQueue.addOperation { [weak self] in 71 | guard let this = self else {return} 72 | guard let action = this.pendingAction else {return} 73 | this.pendingAction = nil 74 | action() 75 | } 76 | } 77 | 78 | } 79 | 80 | private protocol TimerTargetDelegate: class { 81 | func timerFired(for: TimerTarget) 82 | } 83 | 84 | private class TimerTarget { 85 | 86 | weak var delegate: TimerTargetDelegate? 87 | 88 | init(delegate: TimerTargetDelegate) { 89 | self.delegate = delegate 90 | } 91 | 92 | @objc func timerFired(_ timer: Timer) { 93 | delegate?.timerFired(for: self) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Sodes/SodesFoundation/WorkOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkOperation.swift 3 | // SodesFoundation 4 | // 5 | // Created by Jared Sinclair on 7/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | open class WorkOperation: Operation { 12 | 13 | // MARK: Typealiases 14 | 15 | public typealias FinishHandler = () -> Void 16 | 17 | // MARK: Private/public Properties 18 | 19 | private let performQueue: DispatchQueue 20 | private let completion: () -> Void 21 | 22 | // MARK: Init 23 | 24 | public init(completion: @escaping () -> Void) { 25 | self.completion = completion 26 | self.performQueue = DispatchQueue( 27 | label: "com.niceboy.SodesCore.WorkOperation", 28 | qos: .background 29 | ) 30 | super.init() 31 | } 32 | 33 | // MARK: Required Methods for Subclasses 34 | 35 | open func work(_ finish: @escaping () -> Void) { 36 | assertionFailure("Subclasses must override without calling super.") 37 | } 38 | 39 | // MARK: NSOperation Requirements 40 | 41 | override open func start() { 42 | guard !isCancelled else {return} 43 | markAsRunning() 44 | performQueue.async { 45 | self.work { (result) in 46 | onMainQueue { 47 | guard !self.isCancelled else {return} 48 | self.completion() 49 | self.markAsFinished() 50 | } 51 | } 52 | } 53 | } 54 | 55 | private var _finished: Bool = false 56 | override open var isFinished: Bool { 57 | get { return _finished } 58 | set { _finished = newValue } 59 | } 60 | 61 | private var _executing: Bool = false 62 | override open var isExecuting: Bool { 63 | get { return _executing } 64 | set { _executing = newValue } 65 | } 66 | 67 | override open var isAsynchronous: Bool { 68 | return true 69 | } 70 | 71 | private func markAsRunning() { 72 | willChangeValue(for: .isExecuting) 73 | _executing = true 74 | didChangeValue(for: .isExecuting) 75 | } 76 | 77 | private func markAsFinished() { 78 | willChangeValue(for: .isExecuting) 79 | willChangeValue(for: .isFinished) 80 | _executing = false 81 | _finished = true 82 | didChangeValue(for: .isExecuting) 83 | didChangeValue(for: .isFinished) 84 | } 85 | 86 | private func willChangeValue(for key: OperationChangeKey) { 87 | self.willChangeValue(forKey: key.rawValue) 88 | } 89 | 90 | private func didChangeValue(for key: OperationChangeKey) { 91 | self.didChangeValue(forKey: key.rawValue) 92 | } 93 | 94 | } 95 | 96 | private enum OperationChangeKey: String { 97 | case isFinished 98 | case isExecuting 99 | } 100 | -------------------------------------------------------------------------------- /Sodes/SodesFoundationTests/DateFormattingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormattingTests.swift 3 | // Sodes 4 | // 5 | // Created by Jared Sinclair on 8/17/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesFoundation 11 | 12 | class DateFormattingTests: XCTestCase { 13 | 14 | func test_itParsesDateStringWithWeekday() { 15 | let expected = Date(timeIntervalSince1970: 1468130424) 16 | let formatter = RFC822Formatter.sharedFormatter 17 | let actual = formatter.date(from: "Sun, 10 Jul 2016 06:00:24 +0000") 18 | XCTAssertEqual(expected, actual) 19 | } 20 | 21 | func test_itParsesDateStringWithoutWeekday() { 22 | let expected = Date(timeIntervalSince1970: 1468130424) 23 | let formatter = RFC822Formatter.sharedFormatter 24 | let actual = formatter.date(from: "10 Jul 2016 06:00:24 +0000") 25 | XCTAssertEqual(expected, actual) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sodes/SodesFoundationTests/DelegatedHTTPOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DelegatedHTTPOperationTests.swift 3 | // SodesFoundationTests 4 | // 5 | // Created by Jared Sinclair on 8/7/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import SodesFoundation 11 | 12 | class DelegatedHTTPOperationTests: XCTestCase { 13 | 14 | var receivedBytes: Int64 = 0 15 | var delegateQueue: OperationQueue! 16 | 17 | override func setUp() { 18 | receivedBytes = 0 19 | delegateQueue = OperationQueue() 20 | delegateQueue.maxConcurrentOperationCount = 1 21 | } 22 | 23 | func testItFetchesAKnownGoodResource() { 24 | let url = URL(string: "http://jaredsinclair.com/img/pixel-jared.png")! 25 | let exp = expectation(description: "It fetches a known good resource.") 26 | let op = DelegatedHTTPOperation( 27 | url: url, 28 | configuration: .default, 29 | dataDelegate: self, 30 | delegateQueue: delegateQueue, 31 | completion: { (result) in 32 | XCTAssertEqual(OperationQueue.current, OperationQueue.main) 33 | switch result { 34 | case .success(_, let expected, let received): 35 | XCTAssertEqual(expected, received) 36 | XCTAssertEqual(self.receivedBytes, received) 37 | break 38 | case .error(let r, let e): 39 | SodesLog("response: \(r), error: \(e)") 40 | XCTFail() 41 | break 42 | } 43 | XCTAssertEqual(self.receivedBytes, 2134) 44 | exp.fulfill() 45 | }) 46 | op.start() 47 | waitForExpectations(timeout: 10) 48 | } 49 | 50 | } 51 | 52 | extension DelegatedHTTPOperationTests: HTTPOperationDataDelegate { 53 | 54 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveResponse response: HTTPURLResponse) { 55 | // no op 56 | } 57 | 58 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveData data: Data) { 59 | XCTAssertEqual(OperationQueue.current, delegateQueue) 60 | receivedBytes += data.count 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sodes/SodesFoundationTests/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 | -------------------------------------------------------------------------------- /Sodes/SwiftableFileHandle/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 | -------------------------------------------------------------------------------- /Sodes/SwiftableFileHandle/SODSwiftableFileHandle.h: -------------------------------------------------------------------------------- 1 | // 2 | // SODSwiftableFileHandle.h 3 | // SwiftableFileHandle 4 | // 5 | // Created by Jared Sinclair on 8/6/16. 6 | // 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface SODSwiftableFileHandle : NSObject 14 | 15 | - (nullable instancetype)initWithUrl:(NSURL *)fileUrl; 16 | 17 | - (BOOL)writeData:(NSData *)data at:(unsigned long long)location error:(NSError **)error; 18 | 19 | - (nullable NSData *)readDataFromLocation:(unsigned long long)location length:(unsigned long long)length error:(NSError **)error; 20 | 21 | - (void)synchronizeFile; 22 | - (void)closeFile; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /Sodes/SwiftableFileHandle/SODSwiftableFileHandle.m: -------------------------------------------------------------------------------- 1 | // 2 | // SODSwiftableFileHandle.m 3 | // SwiftableFileHandle 4 | // 5 | // Created by Jared Sinclair on 8/6/16. 6 | // 7 | // 8 | 9 | #import "SODSwiftableFileHandle.h" 10 | 11 | @interface SODSwiftableFileHandle() 12 | 13 | @property (nonatomic, assign) BOOL isClosed; 14 | @property (nonatomic, readonly) NSFileHandle *handle; 15 | @property (nonatomic, readonly) NSLock *lock; 16 | 17 | @end 18 | 19 | @implementation SODSwiftableFileHandle 20 | 21 | @synthesize isClosed = _isClosed; 22 | 23 | - (nullable instancetype)initWithUrl:(NSURL *)fileUrl { 24 | NSError *error = nil; 25 | NSFileHandle *handle = [NSFileHandle fileHandleForUpdatingURL:fileUrl error:&error]; 26 | if (error != nil || handle == nil) { 27 | NSLog(@"[SODSwiftableFileHandle initWithUrl:]: Unable to initialize because of error: %@", error); 28 | return nil; 29 | } 30 | self = [super init]; 31 | if (self) { 32 | _handle = handle; 33 | _lock = [NSLock new]; 34 | } 35 | return self; 36 | } 37 | 38 | - (BOOL)writeData:(NSData *)data at:(unsigned long long)location error:( NSError * _Nullable *)error { 39 | 40 | if (self.isClosed) { return NO; } 41 | 42 | BOOL successful; 43 | [self.lock lock]; 44 | @try { 45 | [self.handle seekToFileOffset:location]; 46 | [self.handle writeData:data]; 47 | [self.handle synchronizeFile]; 48 | successful = YES; 49 | } @catch (NSException *exception) { 50 | *error = [NSError errorWithDomain:exception.name code:10001 userInfo:exception.userInfo]; 51 | successful = NO; 52 | } @finally { 53 | // no op 54 | } 55 | [self.lock unlock]; 56 | 57 | return successful; 58 | } 59 | 60 | - (nullable NSData *)readDataFromLocation:(unsigned long long)location length:(unsigned long long)length error:(NSError * _Nullable __autoreleasing *)error { 61 | 62 | if (self.isClosed) { return nil; } 63 | 64 | NSData *data = nil; 65 | [self.lock lock]; 66 | @try { 67 | [self.handle seekToFileOffset:location]; 68 | NSData *readData = [self.handle readDataOfLength:(NSUInteger)length]; 69 | if (readData.length == length) { 70 | data = readData; 71 | } 72 | } @catch (NSException *exception) { 73 | *error = [NSError errorWithDomain:exception.name code:10001 userInfo:exception.userInfo]; 74 | } @finally { 75 | // no op 76 | } 77 | [self.lock unlock]; 78 | 79 | return data; 80 | } 81 | 82 | - (void)synchronizeFile { 83 | if (self.isClosed) { return; } 84 | @try { 85 | [self.handle synchronizeFile]; 86 | } @catch (NSException *exception) { 87 | NSLog(@"SODSwiftableFileHandle.synchronizeFile: %@", exception); 88 | } @finally { 89 | // 90 | } 91 | } 92 | 93 | - (void)closeFile { 94 | if (self.isClosed) { return; } 95 | @try { 96 | [self.handle closeFile]; 97 | } @catch (NSException *exception) { 98 | NSLog(@"SODSwiftableFileHandle.closeFile: %@", exception); 99 | } @finally { 100 | // 101 | } 102 | } 103 | 104 | - (BOOL)isClosed { 105 | BOOL value; 106 | [self.lock lock]; 107 | value = _isClosed; 108 | [self.lock unlock]; 109 | return value; 110 | } 111 | 112 | - (void)setIsClosed:(BOOL)isClosed { 113 | [self.lock lock]; 114 | _isClosed = isClosed; 115 | [self.lock unlock]; 116 | } 117 | 118 | @end 119 | -------------------------------------------------------------------------------- /Sodes/SwiftableFileHandle/SwiftableFileHandle.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftableFileHandle.h 3 | // SwiftableFileHandle 4 | // 5 | // Created by Jared Sinclair on 8/6/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SwiftableFileHandle. 12 | FOUNDATION_EXPORT double SwiftableFileHandleVersionNumber; 13 | 14 | //! Project version string for SwiftableFileHandle. 15 | FOUNDATION_EXPORT const unsigned char SwiftableFileHandleVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #import 20 | 21 | 22 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/screenshot.png --------------------------------------------------------------------------------