├── .gitignore ├── LICENSE ├── MCDownload.gif ├── MCDownloader.podspec ├── MCDownloaderDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── M.C.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── M.C.xcuserdatad │ └── xcschemes │ ├── MCDownloaderDemo.xcscheme │ └── xcschememanagement.plist ├── MCDownloaderDemo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── QKYDelayButton.h ├── QKYDelayButton.m ├── TableViewCell.h ├── TableViewCell.m ├── ViewController.h ├── ViewController.m └── main.m ├── MCDownloaderDemoTests ├── Info.plist └── MCDownloaderDemoTests.m ├── MCDownloaderDemoUITests ├── Info.plist └── MCDownloaderDemoUITests.m ├── README.md └── Source ├── MCDownloadOperation.h ├── MCDownloadOperation.m ├── MCDownloadReceipt.h ├── MCDownloadReceipt.m ├── MCDownloader.h └── MCDownloader.m /.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 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 M.C 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 | -------------------------------------------------------------------------------- /MCDownload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agelessman/MCDownloader/65240f2215464cb410c67602d87e153c8f71ed19/MCDownload.gif -------------------------------------------------------------------------------- /MCDownloader.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint MCDownloader.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "MCDownloader" 19 | s.version = "1.0.1" 20 | s.summary = "A simple and powerful iOS downloader." 21 | 22 | # This description is used to generate tags and improve search results. 23 | # * Think: What does it do? Why did you write it? What is the focus? 24 | # * Try to keep it short, snappy and to the point. 25 | # * Write the description between the DESC delimiters below. 26 | # * Finally, don't worry about the indent, CocoaPods strips it! 27 | # s.description = <<-DESC 28 | # DESC 29 | 30 | s.homepage = "https://github.com/agelessman/MCDownloader" 31 | # s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" 32 | 33 | 34 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 35 | # 36 | # Licensing your code is important. See http://choosealicense.com for more info. 37 | # CocoaPods will detect a license file if there is a named LICENSE* 38 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 39 | # 40 | 41 | s.license = "MIT" 42 | # s.license = { :type => "MIT", :file => "FILE_LICENSE" } 43 | 44 | 45 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 46 | # 47 | # Specify the authors of the library, with email addresses. Email addresses 48 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 49 | # accepts just a name if you'd rather not provide an email address. 50 | # 51 | # Specify a social_media_url where others can refer to, for example a twitter 52 | # profile URL. 53 | # 54 | 55 | s.author = { "MC" => "714080794@qq.com" } 56 | # Or just: s.author = "MC" 57 | # s.authors = { "MC" => "714080794@qq.com" } 58 | # s.social_media_url = "http://twitter.com/MC" 59 | 60 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 61 | # 62 | # If this Pod runs only on iOS or OS X, then specify the platform and 63 | # the deployment target. You can optionally include the target after the platform. 64 | # 65 | 66 | # s.platform = :ios 67 | s.platform = :ios, "8.0" 68 | 69 | # When using multiple platforms 70 | # s.ios.deployment_target = "5.0" 71 | # s.osx.deployment_target = "10.7" 72 | # s.watchos.deployment_target = "2.0" 73 | # s.tvos.deployment_target = "9.0" 74 | 75 | 76 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 77 | # 78 | # Specify the location from where the source should be retrieved. 79 | # Supports git, hg, bzr, svn and HTTP. 80 | # 81 | 82 | s.source = { :git => "https://github.com/agelessman/MCDownloader.git", :tag => "#{s.version}" } 83 | 84 | 85 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 86 | # 87 | # CocoaPods is smart about how it includes source code. For source files 88 | # giving a folder will include any swift, h, m, mm, c & cpp files. 89 | # For header files it will include any header in the folder. 90 | # Not including the public_header_files will make all headers public. 91 | # 92 | 93 | s.source_files = "Source/*.{h,m}" 94 | # s.exclude_files = "Classes/Exclude" 95 | 96 | # s.public_header_files = "Classes/**/*.h" 97 | 98 | 99 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 100 | # 101 | # A list of resources included with the Pod. These are copied into the 102 | # target bundle with a build phase script. Anything else will be cleaned. 103 | # You can preserve files from being cleaned, please don't preserve 104 | # non-essential files like tests, examples and documentation. 105 | # 106 | 107 | # s.resource = "icon.png" 108 | # s.resources = "Resources/*.png" 109 | 110 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 111 | 112 | 113 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 114 | # 115 | # Link your library with frameworks, or libraries. Libraries do not include 116 | # the lib prefix of their name. 117 | # 118 | 119 | # s.framework = "SomeFramework" 120 | # s.frameworks = "SomeFramework", "AnotherFramework" 121 | 122 | # s.library = "iconv" 123 | # s.libraries = "iconv", "xml2" 124 | 125 | 126 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 127 | # 128 | # If your library depends on compiler flags you can set them in the xcconfig hash 129 | # where they will only apply to your library. If you depend on other Podspecs 130 | # you can include multiple dependencies to ensure it works. 131 | 132 | # s.requires_arc = true 133 | 134 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 135 | # s.dependency "JSONKit", "~> 1.4" 136 | 137 | end 138 | -------------------------------------------------------------------------------- /MCDownloaderDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E71DCA251E9B1D2B00A058D2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA241E9B1D2B00A058D2 /* main.m */; }; 11 | E71DCA281E9B1D2B00A058D2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA271E9B1D2B00A058D2 /* AppDelegate.m */; }; 12 | E71DCA2B1E9B1D2B00A058D2 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA2A1E9B1D2B00A058D2 /* ViewController.m */; }; 13 | E71DCA2E1E9B1D2B00A058D2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E71DCA2C1E9B1D2B00A058D2 /* Main.storyboard */; }; 14 | E71DCA301E9B1D2B00A058D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E71DCA2F1E9B1D2B00A058D2 /* Assets.xcassets */; }; 15 | E71DCA331E9B1D2B00A058D2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E71DCA311E9B1D2B00A058D2 /* LaunchScreen.storyboard */; }; 16 | E71DCA3E1E9B1D2C00A058D2 /* MCDownloaderDemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA3D1E9B1D2C00A058D2 /* MCDownloaderDemoTests.m */; }; 17 | E71DCA491E9B1D2C00A058D2 /* MCDownloaderDemoUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA481E9B1D2C00A058D2 /* MCDownloaderDemoUITests.m */; }; 18 | E71DCA5D1E9B1D5F00A058D2 /* MCDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA581E9B1D5F00A058D2 /* MCDownloader.m */; }; 19 | E71DCA5E1E9B1D5F00A058D2 /* MCDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA5A1E9B1D5F00A058D2 /* MCDownloadOperation.m */; }; 20 | E71DCA5F1E9B1D5F00A058D2 /* MCDownloadReceipt.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA5C1E9B1D5F00A058D2 /* MCDownloadReceipt.m */; }; 21 | E71DCA641E9B1DD000A058D2 /* QKYDelayButton.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA611E9B1DD000A058D2 /* QKYDelayButton.m */; }; 22 | E71DCA651E9B1DD000A058D2 /* TableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = E71DCA631E9B1DD000A058D2 /* TableViewCell.m */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | E71DCA3A1E9B1D2C00A058D2 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = E71DCA181E9B1D2B00A058D2 /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = E71DCA1F1E9B1D2B00A058D2; 31 | remoteInfo = MCDownloaderDemo; 32 | }; 33 | E71DCA451E9B1D2C00A058D2 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = E71DCA181E9B1D2B00A058D2 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = E71DCA1F1E9B1D2B00A058D2; 38 | remoteInfo = MCDownloaderDemo; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | E71DCA201E9B1D2B00A058D2 /* MCDownloaderDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MCDownloaderDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | E71DCA241E9B1D2B00A058D2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 45 | E71DCA261E9B1D2B00A058D2 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 46 | E71DCA271E9B1D2B00A058D2 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 47 | E71DCA291E9B1D2B00A058D2 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 48 | E71DCA2A1E9B1D2B00A058D2 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 49 | E71DCA2D1E9B1D2B00A058D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | E71DCA2F1E9B1D2B00A058D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | E71DCA321E9B1D2B00A058D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | E71DCA341E9B1D2B00A058D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | E71DCA391E9B1D2C00A058D2 /* MCDownloaderDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCDownloaderDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | E71DCA3D1E9B1D2C00A058D2 /* MCDownloaderDemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MCDownloaderDemoTests.m; sourceTree = ""; }; 55 | E71DCA3F1E9B1D2C00A058D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | E71DCA441E9B1D2C00A058D2 /* MCDownloaderDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCDownloaderDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | E71DCA481E9B1D2C00A058D2 /* MCDownloaderDemoUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MCDownloaderDemoUITests.m; sourceTree = ""; }; 58 | E71DCA4A1E9B1D2C00A058D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | E71DCA571E9B1D5F00A058D2 /* MCDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCDownloader.h; sourceTree = ""; }; 60 | E71DCA581E9B1D5F00A058D2 /* MCDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCDownloader.m; sourceTree = ""; }; 61 | E71DCA591E9B1D5F00A058D2 /* MCDownloadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCDownloadOperation.h; sourceTree = ""; }; 62 | E71DCA5A1E9B1D5F00A058D2 /* MCDownloadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCDownloadOperation.m; sourceTree = ""; }; 63 | E71DCA5B1E9B1D5F00A058D2 /* MCDownloadReceipt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCDownloadReceipt.h; sourceTree = ""; }; 64 | E71DCA5C1E9B1D5F00A058D2 /* MCDownloadReceipt.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCDownloadReceipt.m; sourceTree = ""; }; 65 | E71DCA601E9B1DD000A058D2 /* QKYDelayButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QKYDelayButton.h; sourceTree = ""; }; 66 | E71DCA611E9B1DD000A058D2 /* QKYDelayButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QKYDelayButton.m; sourceTree = ""; }; 67 | E71DCA621E9B1DD000A058D2 /* TableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TableViewCell.h; sourceTree = ""; }; 68 | E71DCA631E9B1DD000A058D2 /* TableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TableViewCell.m; sourceTree = ""; }; 69 | /* End PBXFileReference section */ 70 | 71 | /* Begin PBXFrameworksBuildPhase section */ 72 | E71DCA1D1E9B1D2B00A058D2 /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | E71DCA361E9B1D2C00A058D2 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | E71DCA411E9B1D2C00A058D2 /* Frameworks */ = { 87 | isa = PBXFrameworksBuildPhase; 88 | buildActionMask = 2147483647; 89 | files = ( 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXFrameworksBuildPhase section */ 94 | 95 | /* Begin PBXGroup section */ 96 | E71DCA171E9B1D2B00A058D2 = { 97 | isa = PBXGroup; 98 | children = ( 99 | E71DCA561E9B1D5F00A058D2 /* Source */, 100 | E71DCA221E9B1D2B00A058D2 /* MCDownloaderDemo */, 101 | E71DCA3C1E9B1D2C00A058D2 /* MCDownloaderDemoTests */, 102 | E71DCA471E9B1D2C00A058D2 /* MCDownloaderDemoUITests */, 103 | E71DCA211E9B1D2B00A058D2 /* Products */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | E71DCA211E9B1D2B00A058D2 /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | E71DCA201E9B1D2B00A058D2 /* MCDownloaderDemo.app */, 111 | E71DCA391E9B1D2C00A058D2 /* MCDownloaderDemoTests.xctest */, 112 | E71DCA441E9B1D2C00A058D2 /* MCDownloaderDemoUITests.xctest */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | E71DCA221E9B1D2B00A058D2 /* MCDownloaderDemo */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | E71DCA601E9B1DD000A058D2 /* QKYDelayButton.h */, 121 | E71DCA611E9B1DD000A058D2 /* QKYDelayButton.m */, 122 | E71DCA621E9B1DD000A058D2 /* TableViewCell.h */, 123 | E71DCA631E9B1DD000A058D2 /* TableViewCell.m */, 124 | E71DCA261E9B1D2B00A058D2 /* AppDelegate.h */, 125 | E71DCA271E9B1D2B00A058D2 /* AppDelegate.m */, 126 | E71DCA291E9B1D2B00A058D2 /* ViewController.h */, 127 | E71DCA2A1E9B1D2B00A058D2 /* ViewController.m */, 128 | E71DCA2C1E9B1D2B00A058D2 /* Main.storyboard */, 129 | E71DCA2F1E9B1D2B00A058D2 /* Assets.xcassets */, 130 | E71DCA311E9B1D2B00A058D2 /* LaunchScreen.storyboard */, 131 | E71DCA341E9B1D2B00A058D2 /* Info.plist */, 132 | E71DCA231E9B1D2B00A058D2 /* Supporting Files */, 133 | ); 134 | path = MCDownloaderDemo; 135 | sourceTree = ""; 136 | }; 137 | E71DCA231E9B1D2B00A058D2 /* Supporting Files */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | E71DCA241E9B1D2B00A058D2 /* main.m */, 141 | ); 142 | name = "Supporting Files"; 143 | sourceTree = ""; 144 | }; 145 | E71DCA3C1E9B1D2C00A058D2 /* MCDownloaderDemoTests */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | E71DCA3D1E9B1D2C00A058D2 /* MCDownloaderDemoTests.m */, 149 | E71DCA3F1E9B1D2C00A058D2 /* Info.plist */, 150 | ); 151 | path = MCDownloaderDemoTests; 152 | sourceTree = ""; 153 | }; 154 | E71DCA471E9B1D2C00A058D2 /* MCDownloaderDemoUITests */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | E71DCA481E9B1D2C00A058D2 /* MCDownloaderDemoUITests.m */, 158 | E71DCA4A1E9B1D2C00A058D2 /* Info.plist */, 159 | ); 160 | path = MCDownloaderDemoUITests; 161 | sourceTree = ""; 162 | }; 163 | E71DCA561E9B1D5F00A058D2 /* Source */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | E71DCA571E9B1D5F00A058D2 /* MCDownloader.h */, 167 | E71DCA581E9B1D5F00A058D2 /* MCDownloader.m */, 168 | E71DCA591E9B1D5F00A058D2 /* MCDownloadOperation.h */, 169 | E71DCA5A1E9B1D5F00A058D2 /* MCDownloadOperation.m */, 170 | E71DCA5B1E9B1D5F00A058D2 /* MCDownloadReceipt.h */, 171 | E71DCA5C1E9B1D5F00A058D2 /* MCDownloadReceipt.m */, 172 | ); 173 | path = Source; 174 | sourceTree = ""; 175 | }; 176 | /* End PBXGroup section */ 177 | 178 | /* Begin PBXNativeTarget section */ 179 | E71DCA1F1E9B1D2B00A058D2 /* MCDownloaderDemo */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = E71DCA4D1E9B1D2C00A058D2 /* Build configuration list for PBXNativeTarget "MCDownloaderDemo" */; 182 | buildPhases = ( 183 | E71DCA1C1E9B1D2B00A058D2 /* Sources */, 184 | E71DCA1D1E9B1D2B00A058D2 /* Frameworks */, 185 | E71DCA1E1E9B1D2B00A058D2 /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | ); 191 | name = MCDownloaderDemo; 192 | productName = MCDownloaderDemo; 193 | productReference = E71DCA201E9B1D2B00A058D2 /* MCDownloaderDemo.app */; 194 | productType = "com.apple.product-type.application"; 195 | }; 196 | E71DCA381E9B1D2C00A058D2 /* MCDownloaderDemoTests */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = E71DCA501E9B1D2C00A058D2 /* Build configuration list for PBXNativeTarget "MCDownloaderDemoTests" */; 199 | buildPhases = ( 200 | E71DCA351E9B1D2C00A058D2 /* Sources */, 201 | E71DCA361E9B1D2C00A058D2 /* Frameworks */, 202 | E71DCA371E9B1D2C00A058D2 /* Resources */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | E71DCA3B1E9B1D2C00A058D2 /* PBXTargetDependency */, 208 | ); 209 | name = MCDownloaderDemoTests; 210 | productName = MCDownloaderDemoTests; 211 | productReference = E71DCA391E9B1D2C00A058D2 /* MCDownloaderDemoTests.xctest */; 212 | productType = "com.apple.product-type.bundle.unit-test"; 213 | }; 214 | E71DCA431E9B1D2C00A058D2 /* MCDownloaderDemoUITests */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = E71DCA531E9B1D2C00A058D2 /* Build configuration list for PBXNativeTarget "MCDownloaderDemoUITests" */; 217 | buildPhases = ( 218 | E71DCA401E9B1D2C00A058D2 /* Sources */, 219 | E71DCA411E9B1D2C00A058D2 /* Frameworks */, 220 | E71DCA421E9B1D2C00A058D2 /* Resources */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | E71DCA461E9B1D2C00A058D2 /* PBXTargetDependency */, 226 | ); 227 | name = MCDownloaderDemoUITests; 228 | productName = MCDownloaderDemoUITests; 229 | productReference = E71DCA441E9B1D2C00A058D2 /* MCDownloaderDemoUITests.xctest */; 230 | productType = "com.apple.product-type.bundle.ui-testing"; 231 | }; 232 | /* End PBXNativeTarget section */ 233 | 234 | /* Begin PBXProject section */ 235 | E71DCA181E9B1D2B00A058D2 /* Project object */ = { 236 | isa = PBXProject; 237 | attributes = { 238 | LastUpgradeCheck = 0820; 239 | ORGANIZATIONNAME = M.C; 240 | TargetAttributes = { 241 | E71DCA1F1E9B1D2B00A058D2 = { 242 | CreatedOnToolsVersion = 8.2.1; 243 | DevelopmentTeam = XC34BSW465; 244 | ProvisioningStyle = Automatic; 245 | }; 246 | E71DCA381E9B1D2C00A058D2 = { 247 | CreatedOnToolsVersion = 8.2.1; 248 | ProvisioningStyle = Automatic; 249 | TestTargetID = E71DCA1F1E9B1D2B00A058D2; 250 | }; 251 | E71DCA431E9B1D2C00A058D2 = { 252 | CreatedOnToolsVersion = 8.2.1; 253 | ProvisioningStyle = Automatic; 254 | TestTargetID = E71DCA1F1E9B1D2B00A058D2; 255 | }; 256 | }; 257 | }; 258 | buildConfigurationList = E71DCA1B1E9B1D2B00A058D2 /* Build configuration list for PBXProject "MCDownloaderDemo" */; 259 | compatibilityVersion = "Xcode 3.2"; 260 | developmentRegion = English; 261 | hasScannedForEncodings = 0; 262 | knownRegions = ( 263 | en, 264 | Base, 265 | ); 266 | mainGroup = E71DCA171E9B1D2B00A058D2; 267 | productRefGroup = E71DCA211E9B1D2B00A058D2 /* Products */; 268 | projectDirPath = ""; 269 | projectRoot = ""; 270 | targets = ( 271 | E71DCA1F1E9B1D2B00A058D2 /* MCDownloaderDemo */, 272 | E71DCA381E9B1D2C00A058D2 /* MCDownloaderDemoTests */, 273 | E71DCA431E9B1D2C00A058D2 /* MCDownloaderDemoUITests */, 274 | ); 275 | }; 276 | /* End PBXProject section */ 277 | 278 | /* Begin PBXResourcesBuildPhase section */ 279 | E71DCA1E1E9B1D2B00A058D2 /* Resources */ = { 280 | isa = PBXResourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | E71DCA331E9B1D2B00A058D2 /* LaunchScreen.storyboard in Resources */, 284 | E71DCA301E9B1D2B00A058D2 /* Assets.xcassets in Resources */, 285 | E71DCA2E1E9B1D2B00A058D2 /* Main.storyboard in Resources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | E71DCA371E9B1D2C00A058D2 /* Resources */ = { 290 | isa = PBXResourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | E71DCA421E9B1D2C00A058D2 /* Resources */ = { 297 | isa = PBXResourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | /* End PBXResourcesBuildPhase section */ 304 | 305 | /* Begin PBXSourcesBuildPhase section */ 306 | E71DCA1C1E9B1D2B00A058D2 /* Sources */ = { 307 | isa = PBXSourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | E71DCA5D1E9B1D5F00A058D2 /* MCDownloader.m in Sources */, 311 | E71DCA2B1E9B1D2B00A058D2 /* ViewController.m in Sources */, 312 | E71DCA5E1E9B1D5F00A058D2 /* MCDownloadOperation.m in Sources */, 313 | E71DCA281E9B1D2B00A058D2 /* AppDelegate.m in Sources */, 314 | E71DCA5F1E9B1D5F00A058D2 /* MCDownloadReceipt.m in Sources */, 315 | E71DCA651E9B1DD000A058D2 /* TableViewCell.m in Sources */, 316 | E71DCA251E9B1D2B00A058D2 /* main.m in Sources */, 317 | E71DCA641E9B1DD000A058D2 /* QKYDelayButton.m in Sources */, 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | E71DCA351E9B1D2C00A058D2 /* Sources */ = { 322 | isa = PBXSourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | E71DCA3E1E9B1D2C00A058D2 /* MCDownloaderDemoTests.m in Sources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | E71DCA401E9B1D2C00A058D2 /* Sources */ = { 330 | isa = PBXSourcesBuildPhase; 331 | buildActionMask = 2147483647; 332 | files = ( 333 | E71DCA491E9B1D2C00A058D2 /* MCDownloaderDemoUITests.m in Sources */, 334 | ); 335 | runOnlyForDeploymentPostprocessing = 0; 336 | }; 337 | /* End PBXSourcesBuildPhase section */ 338 | 339 | /* Begin PBXTargetDependency section */ 340 | E71DCA3B1E9B1D2C00A058D2 /* PBXTargetDependency */ = { 341 | isa = PBXTargetDependency; 342 | target = E71DCA1F1E9B1D2B00A058D2 /* MCDownloaderDemo */; 343 | targetProxy = E71DCA3A1E9B1D2C00A058D2 /* PBXContainerItemProxy */; 344 | }; 345 | E71DCA461E9B1D2C00A058D2 /* PBXTargetDependency */ = { 346 | isa = PBXTargetDependency; 347 | target = E71DCA1F1E9B1D2B00A058D2 /* MCDownloaderDemo */; 348 | targetProxy = E71DCA451E9B1D2C00A058D2 /* PBXContainerItemProxy */; 349 | }; 350 | /* End PBXTargetDependency section */ 351 | 352 | /* Begin PBXVariantGroup section */ 353 | E71DCA2C1E9B1D2B00A058D2 /* Main.storyboard */ = { 354 | isa = PBXVariantGroup; 355 | children = ( 356 | E71DCA2D1E9B1D2B00A058D2 /* Base */, 357 | ); 358 | name = Main.storyboard; 359 | sourceTree = ""; 360 | }; 361 | E71DCA311E9B1D2B00A058D2 /* LaunchScreen.storyboard */ = { 362 | isa = PBXVariantGroup; 363 | children = ( 364 | E71DCA321E9B1D2B00A058D2 /* Base */, 365 | ); 366 | name = LaunchScreen.storyboard; 367 | sourceTree = ""; 368 | }; 369 | /* End PBXVariantGroup section */ 370 | 371 | /* Begin XCBuildConfiguration section */ 372 | E71DCA4B1E9B1D2C00A058D2 /* Debug */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | ALWAYS_SEARCH_USER_PATHS = NO; 376 | CLANG_ANALYZER_NONNULL = YES; 377 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 378 | CLANG_CXX_LIBRARY = "libc++"; 379 | CLANG_ENABLE_MODULES = YES; 380 | CLANG_ENABLE_OBJC_ARC = YES; 381 | CLANG_WARN_BOOL_CONVERSION = YES; 382 | CLANG_WARN_CONSTANT_CONVERSION = YES; 383 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 384 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 385 | CLANG_WARN_EMPTY_BODY = YES; 386 | CLANG_WARN_ENUM_CONVERSION = YES; 387 | CLANG_WARN_INFINITE_RECURSION = YES; 388 | CLANG_WARN_INT_CONVERSION = YES; 389 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 390 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 394 | COPY_PHASE_STRIP = NO; 395 | DEBUG_INFORMATION_FORMAT = dwarf; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | ENABLE_TESTABILITY = YES; 398 | GCC_C_LANGUAGE_STANDARD = gnu99; 399 | GCC_DYNAMIC_NO_PIC = NO; 400 | GCC_NO_COMMON_BLOCKS = YES; 401 | GCC_OPTIMIZATION_LEVEL = 0; 402 | GCC_PREPROCESSOR_DEFINITIONS = ( 403 | "DEBUG=1", 404 | "$(inherited)", 405 | ); 406 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 407 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 408 | GCC_WARN_UNDECLARED_SELECTOR = YES; 409 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 410 | GCC_WARN_UNUSED_FUNCTION = YES; 411 | GCC_WARN_UNUSED_VARIABLE = YES; 412 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 413 | MTL_ENABLE_DEBUG_INFO = YES; 414 | ONLY_ACTIVE_ARCH = YES; 415 | SDKROOT = iphoneos; 416 | }; 417 | name = Debug; 418 | }; 419 | E71DCA4C1E9B1D2C00A058D2 /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ALWAYS_SEARCH_USER_PATHS = NO; 423 | CLANG_ANALYZER_NONNULL = YES; 424 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 425 | CLANG_CXX_LIBRARY = "libc++"; 426 | CLANG_ENABLE_MODULES = YES; 427 | CLANG_ENABLE_OBJC_ARC = YES; 428 | CLANG_WARN_BOOL_CONVERSION = YES; 429 | CLANG_WARN_CONSTANT_CONVERSION = YES; 430 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 431 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 432 | CLANG_WARN_EMPTY_BODY = YES; 433 | CLANG_WARN_ENUM_CONVERSION = YES; 434 | CLANG_WARN_INFINITE_RECURSION = YES; 435 | CLANG_WARN_INT_CONVERSION = YES; 436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 437 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 438 | CLANG_WARN_UNREACHABLE_CODE = YES; 439 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 440 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 441 | COPY_PHASE_STRIP = NO; 442 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 443 | ENABLE_NS_ASSERTIONS = NO; 444 | ENABLE_STRICT_OBJC_MSGSEND = YES; 445 | GCC_C_LANGUAGE_STANDARD = gnu99; 446 | GCC_NO_COMMON_BLOCKS = YES; 447 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 448 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 449 | GCC_WARN_UNDECLARED_SELECTOR = YES; 450 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 451 | GCC_WARN_UNUSED_FUNCTION = YES; 452 | GCC_WARN_UNUSED_VARIABLE = YES; 453 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 454 | MTL_ENABLE_DEBUG_INFO = NO; 455 | SDKROOT = iphoneos; 456 | VALIDATE_PRODUCT = YES; 457 | }; 458 | name = Release; 459 | }; 460 | E71DCA4E1E9B1D2C00A058D2 /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 464 | DEVELOPMENT_TEAM = XC34BSW465; 465 | INFOPLIST_FILE = MCDownloaderDemo/Info.plist; 466 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 467 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 468 | PRODUCT_BUNDLE_IDENTIFIER = M.C.MCDownloaderDemo; 469 | PRODUCT_NAME = "$(TARGET_NAME)"; 470 | }; 471 | name = Debug; 472 | }; 473 | E71DCA4F1E9B1D2C00A058D2 /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 477 | DEVELOPMENT_TEAM = XC34BSW465; 478 | INFOPLIST_FILE = MCDownloaderDemo/Info.plist; 479 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 480 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 481 | PRODUCT_BUNDLE_IDENTIFIER = M.C.MCDownloaderDemo; 482 | PRODUCT_NAME = "$(TARGET_NAME)"; 483 | }; 484 | name = Release; 485 | }; 486 | E71DCA511E9B1D2C00A058D2 /* Debug */ = { 487 | isa = XCBuildConfiguration; 488 | buildSettings = { 489 | BUNDLE_LOADER = "$(TEST_HOST)"; 490 | INFOPLIST_FILE = MCDownloaderDemoTests/Info.plist; 491 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 492 | PRODUCT_BUNDLE_IDENTIFIER = M.C.MCDownloaderDemoTests; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MCDownloaderDemo.app/MCDownloaderDemo"; 495 | }; 496 | name = Debug; 497 | }; 498 | E71DCA521E9B1D2C00A058D2 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | BUNDLE_LOADER = "$(TEST_HOST)"; 502 | INFOPLIST_FILE = MCDownloaderDemoTests/Info.plist; 503 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 504 | PRODUCT_BUNDLE_IDENTIFIER = M.C.MCDownloaderDemoTests; 505 | PRODUCT_NAME = "$(TARGET_NAME)"; 506 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MCDownloaderDemo.app/MCDownloaderDemo"; 507 | }; 508 | name = Release; 509 | }; 510 | E71DCA541E9B1D2C00A058D2 /* Debug */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | INFOPLIST_FILE = MCDownloaderDemoUITests/Info.plist; 514 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 515 | PRODUCT_BUNDLE_IDENTIFIER = M.C.MCDownloaderDemoUITests; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | TEST_TARGET_NAME = MCDownloaderDemo; 518 | }; 519 | name = Debug; 520 | }; 521 | E71DCA551E9B1D2C00A058D2 /* Release */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | INFOPLIST_FILE = MCDownloaderDemoUITests/Info.plist; 525 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 526 | PRODUCT_BUNDLE_IDENTIFIER = M.C.MCDownloaderDemoUITests; 527 | PRODUCT_NAME = "$(TARGET_NAME)"; 528 | TEST_TARGET_NAME = MCDownloaderDemo; 529 | }; 530 | name = Release; 531 | }; 532 | /* End XCBuildConfiguration section */ 533 | 534 | /* Begin XCConfigurationList section */ 535 | E71DCA1B1E9B1D2B00A058D2 /* Build configuration list for PBXProject "MCDownloaderDemo" */ = { 536 | isa = XCConfigurationList; 537 | buildConfigurations = ( 538 | E71DCA4B1E9B1D2C00A058D2 /* Debug */, 539 | E71DCA4C1E9B1D2C00A058D2 /* Release */, 540 | ); 541 | defaultConfigurationIsVisible = 0; 542 | defaultConfigurationName = Release; 543 | }; 544 | E71DCA4D1E9B1D2C00A058D2 /* Build configuration list for PBXNativeTarget "MCDownloaderDemo" */ = { 545 | isa = XCConfigurationList; 546 | buildConfigurations = ( 547 | E71DCA4E1E9B1D2C00A058D2 /* Debug */, 548 | E71DCA4F1E9B1D2C00A058D2 /* Release */, 549 | ); 550 | defaultConfigurationIsVisible = 0; 551 | defaultConfigurationName = Release; 552 | }; 553 | E71DCA501E9B1D2C00A058D2 /* Build configuration list for PBXNativeTarget "MCDownloaderDemoTests" */ = { 554 | isa = XCConfigurationList; 555 | buildConfigurations = ( 556 | E71DCA511E9B1D2C00A058D2 /* Debug */, 557 | E71DCA521E9B1D2C00A058D2 /* Release */, 558 | ); 559 | defaultConfigurationIsVisible = 0; 560 | defaultConfigurationName = Release; 561 | }; 562 | E71DCA531E9B1D2C00A058D2 /* Build configuration list for PBXNativeTarget "MCDownloaderDemoUITests" */ = { 563 | isa = XCConfigurationList; 564 | buildConfigurations = ( 565 | E71DCA541E9B1D2C00A058D2 /* Debug */, 566 | E71DCA551E9B1D2C00A058D2 /* Release */, 567 | ); 568 | defaultConfigurationIsVisible = 0; 569 | defaultConfigurationName = Release; 570 | }; 571 | /* End XCConfigurationList section */ 572 | }; 573 | rootObject = E71DCA181E9B1D2B00A058D2 /* Project object */; 574 | } 575 | -------------------------------------------------------------------------------- /MCDownloaderDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MCDownloaderDemo.xcodeproj/project.xcworkspace/xcuserdata/M.C.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agelessman/MCDownloader/65240f2215464cb410c67602d87e153c8f71ed19/MCDownloaderDemo.xcodeproj/project.xcworkspace/xcuserdata/M.C.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MCDownloaderDemo.xcodeproj/xcuserdata/M.C.xcuserdatad/xcschemes/MCDownloaderDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /MCDownloaderDemo.xcodeproj/xcuserdata/M.C.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MCDownloaderDemo.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E71DCA1F1E9B1D2B00A058D2 16 | 17 | primary 18 | 19 | 20 | E71DCA381E9B1D2C00A058D2 21 | 22 | primary 23 | 24 | 25 | E71DCA431E9B1D2C00A058D2 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MCDownloaderDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // MCDownloaderDemo 4 | // 5 | // Created by M.C on 17/4/10. 6 | // Copyright © 2017年 M.C. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /MCDownloaderDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // MCDownloaderDemo 4 | // 5 | // Created by M.C on 17/4/10. 6 | // Copyright © 2017年 M.C. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // 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. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // 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. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // 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. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // 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. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /MCDownloaderDemo/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 | } -------------------------------------------------------------------------------- /MCDownloaderDemo/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 | -------------------------------------------------------------------------------- /MCDownloaderDemo/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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 61 | 71 | 81 | 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 | -------------------------------------------------------------------------------- /MCDownloaderDemo/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 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /MCDownloaderDemo/QKYDelayButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // QKYDelayButton.h 3 | // qikeyun 4 | // 5 | // Created by 马超 on 16/6/4. 6 | // Copyright © 2016年 Jerome. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface QKYDelayButton : UIButton 12 | 13 | @property (nonatomic,assign)NSTimeInterval clickDurationTime; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /MCDownloaderDemo/QKYDelayButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // QKYDelayButton.m 3 | // qikeyun 4 | // 5 | // Created by 马超 on 16/6/4. 6 | // Copyright © 2016年 Jerome. All rights reserved. 7 | // 8 | 9 | #import "QKYDelayButton.h" 10 | 11 | static NSTimeInterval defaultDuration = 1.0f; 12 | 13 | static BOOL _isIgnoreEvent = NO; 14 | 15 | static void resetState() { 16 | 17 | _isIgnoreEvent = NO; 18 | } 19 | 20 | 21 | @implementation QKYDelayButton 22 | 23 | - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event 24 | { 25 | if ([self isKindOfClass:[UIButton class]]) { 26 | 27 | self.clickDurationTime = self.clickDurationTime == 0 ? defaultDuration : self.clickDurationTime; 28 | 29 | if (_isIgnoreEvent) { 30 | 31 | return; 32 | } 33 | else if (self.clickDurationTime > 0) { 34 | 35 | _isIgnoreEvent = YES; 36 | 37 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.clickDurationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 38 | 39 | resetState(); 40 | }); 41 | 42 | [super sendAction:action to:target forEvent:event]; 43 | } 44 | } 45 | else { 46 | 47 | [super sendAction:action to:target forEvent:event]; 48 | } 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /MCDownloaderDemo/TableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.h 3 | // MCDownloadManager 4 | // 5 | // Created by 马超 on 16/9/6. 6 | // Copyright © 2016年 qikeyun. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "QKYDelayButton.h" 11 | 12 | @class TableViewCell; 13 | @protocol TableViewCellDelegate 14 | 15 | - (void)cell:(TableViewCell *)cell didClickedBtn:(UIButton *)btn; 16 | 17 | @end 18 | 19 | @interface TableViewCell : UITableViewCell 20 | @property (weak, nonatomic) IBOutlet UIProgressView *progressView; 21 | @property (weak, nonatomic) IBOutlet UILabel *nameLabel; 22 | @property (weak, nonatomic) IBOutlet QKYDelayButton *button; 23 | @property (weak, nonatomic) IBOutlet UILabel *bytesLable; 24 | @property (weak, nonatomic) IBOutlet UILabel *speedLable; 25 | 26 | 27 | @property (nonatomic, weak) id delegate; 28 | @property (nonatomic,copy)NSString *url; 29 | @end 30 | -------------------------------------------------------------------------------- /MCDownloaderDemo/TableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.m 3 | // MCDownloadManager 4 | // 5 | // Created by 马超 on 16/9/6. 6 | // Copyright © 2016年 qikeyun. All rights reserved. 7 | // 8 | 9 | #import "TableViewCell.h" 10 | #import "MCDownloader.h" 11 | 12 | 13 | 14 | @implementation TableViewCell 15 | 16 | - (void)awakeFromNib { 17 | [super awakeFromNib]; 18 | // Initialization code 19 | 20 | self.button.clipsToBounds = YES; 21 | self.button.layer.cornerRadius = 10; 22 | self.button.layer.borderWidth = 1; 23 | self.button.layer.borderColor = [UIColor orangeColor].CGColor; 24 | [self.button setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal]; 25 | 26 | } 27 | 28 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated { 29 | [super setSelected:selected animated:animated]; 30 | 31 | // Configure the view for the selected state 32 | } 33 | 34 | - (void)setUrl:(NSString *)url { 35 | _url = url; 36 | 37 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:url]; 38 | 39 | receipt.customFilePathBlock = ^NSString * _Nullable(MCDownloadReceipt * _Nullable receipt) { 40 | NSString *cacheDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject; 41 | NSString *cacheFolderPath = [cacheDir stringByAppendingPathComponent:@"我自己写的"]; 42 | return [cacheFolderPath stringByAppendingPathComponent:url.lastPathComponent]; 43 | }; 44 | 45 | // NSLog(@"%@", receipt.filePath); 46 | self.nameLabel.text = receipt.truename; 47 | self.speedLable.text = nil; 48 | self.bytesLable.text = nil; 49 | self.progressView.progress = 0; 50 | self.progressView.progress = receipt.progress.fractionCompleted; 51 | 52 | // self.imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfFile:receipt.filePath]]; 53 | 54 | if (receipt.state == MCDownloadStateDownloading || receipt.state == MCDownloadStateWillResume) { 55 | [self.button setTitle:@"Stop" forState:UIControlStateNormal]; 56 | }else if (receipt.state == MCDownloadStateCompleted) { 57 | [self.button setTitle:@"Play" forState:UIControlStateNormal]; 58 | self.nameLabel.text = @"Download Finished"; 59 | }else { 60 | [self.button setTitle:@"Start" forState:UIControlStateNormal]; 61 | } 62 | 63 | __weak typeof(receipt) weakReceipt = receipt; 64 | receipt.downloaderProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL) { 65 | __strong typeof(weakReceipt) strongReceipt = weakReceipt; 66 | if ([targetURL.absoluteString isEqualToString:self.url]) { 67 | [self.button setTitle:@"Stop" forState:UIControlStateNormal]; 68 | self.bytesLable.text = [NSString stringWithFormat:@"%0.1fm/%0.1fm", receivedSize/1024.0/1024,expectedSize/1024.0/1024]; 69 | self.progressView.progress = (receivedSize/1024.0/1024) / (expectedSize/1024.0/1024); 70 | self.speedLable.text = [NSString stringWithFormat:@"%@/s", strongReceipt.speed ?: @"0"]; 71 | } 72 | 73 | }; 74 | 75 | receipt.downloaderCompletedBlock = ^(MCDownloadReceipt *receipt, NSError * _Nullable error, BOOL finished) { 76 | if (error) { 77 | [self.button setTitle:@"Start" forState:UIControlStateNormal]; 78 | self.nameLabel.text = @"Download Failure"; 79 | }else { 80 | [self.button setTitle:@"Play" forState:UIControlStateNormal]; 81 | self.nameLabel.text = @"Download Finished"; 82 | } 83 | 84 | }; 85 | } 86 | - (IBAction)buttonAction:(UIButton *)sender { 87 | 88 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.url]; 89 | if (receipt.state == MCDownloadStateDownloading || receipt.state == MCDownloadStateWillResume) { 90 | 91 | [[MCDownloader sharedDownloader] cancel:receipt completed:^{ 92 | [self.button setTitle:@"Start" forState:UIControlStateNormal]; 93 | }]; 94 | }else if (receipt.state == MCDownloadStateCompleted) { 95 | 96 | if ([self.delegate respondsToSelector:@selector(cell:didClickedBtn:)]) { 97 | [self.delegate cell:self didClickedBtn:sender]; 98 | } 99 | }else { 100 | [self.button setTitle:@"Stop" forState:UIControlStateNormal]; 101 | [self download]; 102 | } 103 | 104 | } 105 | 106 | - (void)download { 107 | 108 | [[MCDownloader sharedDownloader] downloadDataWithURL:[NSURL URLWithString:self.url] progress:^(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL) { 109 | 110 | } completed:^(MCDownloadReceipt *receipt, NSError * _Nullable error, BOOL finished) { 111 | NSLog(@"==%@", error.description); 112 | }]; 113 | 114 | 115 | } 116 | @end 117 | -------------------------------------------------------------------------------- /MCDownloaderDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // MCDownloadManager 4 | // 5 | // Created by 马超 on 16/9/5. 6 | // Copyright © 2016年 qikeyun. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UITableViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /MCDownloaderDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // MCDownloadManager 4 | // 5 | // Created by 马超 on 16/9/5. 6 | // Copyright © 2016年 qikeyun. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "TableViewCell.h" 11 | #import 12 | #import "MCDownloader.h" 13 | 14 | 15 | 16 | @interface ViewController () 17 | @property (weak, nonatomic) IBOutlet UILabel *label; 18 | 19 | 20 | 21 | @property (strong, nonatomic) NSMutableArray *urls; 22 | @end 23 | 24 | @implementation ViewController 25 | 26 | - (NSMutableArray *)urls 27 | { 28 | if (!_urls) { 29 | self.urls = [NSMutableArray array]; 30 | for (int i = 1; i<=10; i++) { 31 | [self.urls addObject:[NSString stringWithFormat:@"http://120.25.226.186:32812/resources/videos/minion_%02d.mp4", i]]; 32 | 33 | // [self.urls addObject:@"http://localhost/MJDownload-master.zip"]; 34 | } 35 | } 36 | return _urls; 37 | } 38 | - (void)viewDidLoad { 39 | [super viewDidLoad]; 40 | // Do any additional setup after loading the view, typically from a nib. 41 | self.view.backgroundColor = [UIColor whiteColor]; 42 | [[MCDownloader sharedDownloader] removeAndClearAll]; 43 | // [MCDownloader sharedDownloader].maxConcurrentDownloads = 1; 44 | } 45 | 46 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 47 | return [self urls].count; 48 | } 49 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 50 | return 100; 51 | } 52 | 53 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 54 | TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; 55 | cell.url = [self urls][indexPath.row]; 56 | cell.delegate = self; 57 | return cell; 58 | } 59 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 60 | return YES; 61 | } 62 | - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { 63 | return UITableViewCellEditingStyleDelete; 64 | } 65 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 66 | if (editingStyle == UITableViewCellEditingStyleDelete) { 67 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:[self urls][indexPath.row]]; 68 | [[MCDownloader sharedDownloader] remove:receipt completed:^{ 69 | [self.tableView reloadData]; 70 | }]; 71 | 72 | } 73 | } 74 | 75 | - (IBAction)nextAction:(UIBarButtonItem *)sender { 76 | 77 | 78 | 79 | NSArray *urls = [self urls]; 80 | 81 | if ([sender.title isEqualToString:@"Start"]) { 82 | 83 | sender.enabled = NO; 84 | 85 | for (NSString *url in urls) { 86 | [[MCDownloader sharedDownloader] downloadDataWithURL:[NSURL URLWithString:url] progress:^(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL) { 87 | 88 | } completed:^(MCDownloadReceipt * _Nullable receipt, NSError * _Nullable error, BOOL finished) { 89 | NSLog(@"==%@", error.description); 90 | }]; 91 | 92 | } 93 | 94 | sender.enabled = YES; 95 | sender.title = @"Stop"; 96 | } else { 97 | 98 | sender.enabled = NO; 99 | 100 | [[MCDownloader sharedDownloader] cancelAllDownloads]; 101 | 102 | sender.enabled = YES; 103 | sender.title = @"Start"; 104 | } 105 | [self.tableView reloadData]; 106 | } 107 | 108 | 109 | - (void)cell:(TableViewCell *)cell didClickedBtn:(UIButton *)btn { 110 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:cell.url]; 111 | UIViewController *vc = [UIApplication sharedApplication].keyWindow.rootViewController; 112 | MPMoviePlayerViewController *mpc = [[MPMoviePlayerViewController alloc] initWithContentURL:[NSURL fileURLWithPath:receipt.filePath]]; 113 | [vc presentViewController:mpc animated:YES completion:nil]; 114 | } 115 | 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /MCDownloaderDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MCDownloaderDemo 4 | // 5 | // Created by M.C on 17/4/10. 6 | // Copyright © 2017年 M.C. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MCDownloaderDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MCDownloaderDemoTests/MCDownloaderDemoTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloaderDemoTests.m 3 | // MCDownloaderDemoTests 4 | // 5 | // Created by M.C on 17/4/10. 6 | // Copyright © 2017年 M.C. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MCDownloaderDemoTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation MCDownloaderDemoTests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | - (void)tearDown { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testExample { 28 | // This is an example of a functional test case. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | - (void)testPerformanceExample { 33 | // This is an example of a performance test case. 34 | [self measureBlock:^{ 35 | // Put the code you want to measure the time of here. 36 | }]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /MCDownloaderDemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MCDownloaderDemoUITests/MCDownloaderDemoUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloaderDemoUITests.m 3 | // MCDownloaderDemoUITests 4 | // 5 | // Created by M.C on 17/4/10. 6 | // Copyright © 2017年 M.C. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MCDownloaderDemoUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation MCDownloaderDemoUITests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | 22 | // In UI tests it is usually best to stop immediately when a failure occurs. 23 | self.continueAfterFailure = NO; 24 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 25 | [[[XCUIApplication alloc] init] launch]; 26 | 27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 28 | } 29 | 30 | - (void)tearDown { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | [super tearDown]; 33 | } 34 | 35 | - (void)testExample { 36 | // Use recording to get started writing UI tests. 37 | // Use XCTAssert and related functions to verify your tests produce the correct results. 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCDownloader 2 | A simple and powerful iOS downloader. [中文简介](http://www.jianshu.com/p/062327c5846a) 3 | 4 | ![](MCDownload.gif) 5 | 6 | 7 | ## Installation 8 | 9 | Copy the source file to your project. 10 | 11 | 12 | ## Usage 13 | ### Start the download 14 | 15 | [[MCDownloader sharedDownloader] downloadDataWithURL:[NSURL URLWithString:url] progress:^(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL) { 16 | 17 | } completed:^(MCDownloadReceipt * _Nullable receipt, NSError * _Nullable error, BOOL finished) { 18 | NSLog(@"==%@", error.description); 19 | }]; 20 | 21 | ### Stop the download 22 | 23 | [[MCDownloader sharedDownloader] cancel:receipt completed:^{ 24 | [self.button setTitle:@"Start" forState:UIControlStateNormal]; 25 | }]; 26 | 27 | ### Remove the download 28 | 29 | [[MCDownloader sharedDownloader] remove:receipt completed:^{ 30 | [self.tableView reloadData]; 31 | }]; 32 | 33 | ### Get the download information 34 | 35 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:url]; 36 | 37 | ### Cancel and remove all downloads 38 | 39 | [[MCDownloader sharedDownloader] cancelAllDownloads]; 40 | 41 | [[MCDownloader sharedDownloader] removeAndClearAll]; 42 | 43 | ## License 44 | MCDownloader is released under an MIT license. See License.md for more information. -------------------------------------------------------------------------------- /Source/MCDownloadOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloadOperation.h 3 | // MCDownloadManager 4 | // 5 | // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com) 6 | // Copyright © 2017年 qikeyun. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | #import 27 | #import "MCDownloader.h" 28 | 29 | 30 | extern NSString * _Nonnull const MCDownloadStartNotification; 31 | extern NSString * _Nonnull const MCDownloadReceiveResponseNotification; 32 | extern NSString * _Nonnull const MCDownloadStopNotification; 33 | extern NSString * _Nonnull const MCDownloadFinishNotification; 34 | 35 | /** 36 | Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol 37 | */ 38 | @protocol MCDownloaderOperationInterface 39 | 40 | - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request 41 | inSession:(nullable NSURLSession *)session; 42 | 43 | - (nullable id)addHandlersForProgress:(nullable MCDownloaderProgressBlock)progressBlock 44 | completed:(nullable MCDownloaderCompletedBlock)completedBlock; 45 | 46 | @end 47 | 48 | 49 | @interface MCDownloadOperation : NSOperation 50 | 51 | /** 52 | * The request used by the operation's task. 53 | */ 54 | @property (strong, nonatomic, nullable) NSURLRequest *request; 55 | 56 | /** 57 | * The operation's task 58 | */ 59 | @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; 60 | 61 | 62 | /** 63 | * The expected size of data. 64 | */ 65 | @property (assign, nonatomic) NSInteger expectedSize; 66 | 67 | /** 68 | * The response returned by the operation's connection. 69 | */ 70 | @property (strong, nonatomic, nullable) NSURLResponse *response; 71 | 72 | /** 73 | * Initializes a `MCDownloadOperation` object 74 | * 75 | * @see MCDownloadOperation 76 | * 77 | * @param request the receipt 78 | * @param session the URL session in which this operation will run 79 | * 80 | * @return the initialized instance 81 | */ 82 | - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request 83 | inSession:(nullable NSURLSession *)session NS_DESIGNATED_INITIALIZER; 84 | 85 | /** 86 | * Adds handlers for progress and completion. Returns a tokent that can be passed to -cancel: to cancel this set of 87 | * callbacks. 88 | * 89 | * @param progressBlock the block executed when a new chunk of data arrives. 90 | * @note the progress block is executed on a background queue 91 | * @param completedBlock the block executed when the download is done. 92 | * @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue 93 | * 94 | * @return the token to use to cancel this set of handlers 95 | */ 96 | - (nullable id)addHandlersForProgress:(nullable MCDownloaderProgressBlock)progressBlock 97 | completed:(nullable MCDownloaderCompletedBlock)completedBlock; 98 | 99 | /** 100 | * Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled. 101 | * 102 | * @param token the token representing a set of callbacks to cancel 103 | * 104 | * @return YES if the operation was stopped because this was the last token to be canceled. NO otherwise. 105 | */ 106 | - (BOOL)cancel:(nullable id)token; 107 | @end 108 | -------------------------------------------------------------------------------- /Source/MCDownloadOperation.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloadOperation.m 3 | // MCDownloadManager 4 | // 5 | // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com) 6 | // Copyright © 2017年 qikeyun. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import "MCDownloadOperation.h" 28 | 29 | 30 | NS_ASSUME_NONNULL_BEGIN 31 | 32 | NSString *const MCDownloadStartNotification = @"MCDownloadStartNotification"; 33 | NSString *const MCDownloadReceiveResponseNotification = @"MCDownloadReceiveResponseNotification"; 34 | NSString *const MCDownloadStopNotification = @"MCDownloadStopNotification"; 35 | NSString *const MCDownloadFinishNotification = @"MCDownloadFinishNotification"; 36 | 37 | static NSString *const kProgressCallbackKey = @"progress"; 38 | static NSString *const kCompletedCallbackKey = @"completed"; 39 | 40 | typedef NSMutableDictionary MCCallbacksDictionary; 41 | 42 | 43 | @interface MCDownloadOperation () 44 | 45 | @property (strong, nonatomic, nonnull) NSMutableArray *callbackBlocks; 46 | 47 | @property (assign, nonatomic, getter = isExecuting) BOOL executing; 48 | @property (assign, nonatomic, getter = isFinished) BOOL finished; 49 | 50 | // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run 51 | // the task associated with this operation 52 | @property (weak, nonatomic, nullable) NSURLSession *unownedSession; 53 | // This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one 54 | @property (strong, nonatomic, nullable) NSURLSession *ownedSession; 55 | 56 | 57 | 58 | @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask; 59 | 60 | @property (strong, nonatomic, nullable) dispatch_queue_t barrierQueue; 61 | 62 | @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; 63 | 64 | @property (assign, nonatomic) long long totalBytesWritten; 65 | @property (assign, nonatomic) long long totalBytesExpectedToWrite; 66 | 67 | @property (strong, nonatomic) MCDownloadReceipt *receipt; 68 | @end 69 | 70 | @implementation MCDownloadOperation 71 | { 72 | BOOL responseFromCached; 73 | } 74 | 75 | @synthesize executing = _executing; 76 | @synthesize finished = _finished; 77 | 78 | - (MCDownloadReceipt *)receipt { 79 | if (_receipt == nil) { 80 | _receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.request.URL.absoluteString]; 81 | } 82 | return _receipt; 83 | } 84 | - (nonnull instancetype)init { 85 | return [self initWithRequest:nil inSession:nil]; 86 | } 87 | 88 | - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session { 89 | if ((self = [super init])) { 90 | _request = [request copy]; 91 | _callbackBlocks = [NSMutableArray new]; 92 | _executing = NO; 93 | _finished = NO; 94 | _expectedSize = 0; 95 | _unownedSession = session; 96 | responseFromCached = YES; // Initially wrong until `- URLSession:dataTask:willCacheResponse:completionHandler: is called or not called 97 | _barrierQueue = dispatch_queue_create("com.machao.MCDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT); 98 | 99 | [self.receipt setState:MCDownloadStateWillResume]; 100 | } 101 | return self; 102 | } 103 | 104 | - (void)dealloc { 105 | 106 | } 107 | 108 | - (nullable id)addHandlersForProgress:(nullable MCDownloaderProgressBlock)progressBlock 109 | completed:(nullable MCDownloaderCompletedBlock)completedBlock { 110 | MCCallbacksDictionary *callbacks = [NSMutableDictionary new]; 111 | if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; 112 | if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; 113 | dispatch_barrier_async(self.barrierQueue, ^{ 114 | [self.callbackBlocks addObject:callbacks]; 115 | }); 116 | return callbacks; 117 | } 118 | 119 | - (nullable NSArray *)callbacksForKey:(NSString *)key { 120 | __block NSMutableArray *callbacks = nil; 121 | dispatch_sync(self.barrierQueue, ^{ 122 | // We need to remove [NSNull null] because there might not always be a progress block for each callback 123 | callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy]; 124 | [callbacks removeObjectIdenticalTo:[NSNull null]]; 125 | }); 126 | return [callbacks copy]; // strip mutability here 127 | } 128 | 129 | - (BOOL)cancel:(nullable id)token { 130 | __block BOOL shouldCancel = NO; 131 | dispatch_barrier_sync(self.barrierQueue, ^{ 132 | [self.callbackBlocks removeAllObjects]; 133 | if (self.callbackBlocks.count == 0) { 134 | shouldCancel = YES; 135 | } 136 | }); 137 | if (shouldCancel) { 138 | [self cancel]; 139 | } 140 | return shouldCancel; 141 | } 142 | 143 | - (void)start { 144 | @synchronized (self) { 145 | if (self.isCancelled) { 146 | self.finished = YES; 147 | [self reset]; 148 | return; 149 | } 150 | 151 | #if TARGET_OS_IOS 152 | Class UIApplicationClass = NSClassFromString(@"UIApplication"); 153 | BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; 154 | if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { 155 | __weak __typeof__ (self) wself = self; 156 | UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; 157 | self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ 158 | __strong __typeof (wself) sself = wself; 159 | 160 | if (sself) { 161 | [sself cancel]; 162 | 163 | [app endBackgroundTask:sself.backgroundTaskId]; 164 | sself.backgroundTaskId = UIBackgroundTaskInvalid; 165 | } 166 | }]; 167 | } 168 | #endif 169 | NSURLSession *session = self.unownedSession; 170 | if (!self.unownedSession) { 171 | NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; 172 | sessionConfig.timeoutIntervalForRequest = 15; 173 | 174 | /** 175 | * Create the session for this task 176 | * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate 177 | * method calls and completion handler calls. 178 | */ 179 | self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig 180 | delegate:self 181 | delegateQueue:nil]; 182 | session = self.ownedSession; 183 | } 184 | 185 | self.dataTask = [session dataTaskWithRequest:self.request]; 186 | self.executing = YES; 187 | } 188 | 189 | [self.dataTask resume]; 190 | 191 | if (self.dataTask) { 192 | for (MCDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { 193 | progressBlock(0, NSURLResponseUnknownLength, 0, self.request.URL); 194 | } 195 | [self.receipt setState:MCDownloadStateDownloading]; 196 | dispatch_async(dispatch_get_main_queue(), ^{ 197 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStartNotification object:self]; 198 | }); 199 | } else { 200 | [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]]; 201 | } 202 | 203 | #if TARGET_OS_IOS 204 | Class UIApplicationClass = NSClassFromString(@"UIApplication"); 205 | if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { 206 | return; 207 | } 208 | if (self.backgroundTaskId != UIBackgroundTaskInvalid) { 209 | UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; 210 | [app endBackgroundTask:self.backgroundTaskId]; 211 | self.backgroundTaskId = UIBackgroundTaskInvalid; 212 | } 213 | #endif 214 | } 215 | 216 | - (void)cancel { 217 | @synchronized (self) { 218 | [self cancelInternal]; 219 | } 220 | } 221 | 222 | - (void)cancelInternal { 223 | if (self.isFinished) return; 224 | [super cancel]; 225 | 226 | if (self.dataTask) { 227 | [self.dataTask cancel]; 228 | [self.receipt setState:MCDownloadStateNone]; 229 | dispatch_async(dispatch_get_main_queue(), ^{ 230 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStopNotification object:self]; 231 | }); 232 | 233 | // As we cancelled the connection, its callback won't be called and thus won't 234 | // maintain the isFinished and isExecuting flags. 235 | if (self.isExecuting) self.executing = NO; 236 | if (!self.isFinished) self.finished = YES; 237 | } 238 | 239 | [self reset]; 240 | } 241 | 242 | - (void)done { 243 | self.finished = YES; 244 | self.executing = NO; 245 | [self reset]; 246 | } 247 | 248 | - (void)reset { 249 | dispatch_barrier_async(self.barrierQueue, ^{ 250 | [self.callbackBlocks removeAllObjects]; 251 | }); 252 | self.dataTask = nil; 253 | if (self.ownedSession) { 254 | [self.ownedSession invalidateAndCancel]; 255 | self.ownedSession = nil; 256 | } 257 | } 258 | 259 | - (void)setFinished:(BOOL)finished { 260 | [self willChangeValueForKey:@"isFinished"]; 261 | _finished = finished; 262 | [self didChangeValueForKey:@"isFinished"]; 263 | } 264 | 265 | - (void)setExecuting:(BOOL)executing { 266 | [self willChangeValueForKey:@"isExecuting"]; 267 | _executing = executing; 268 | [self didChangeValueForKey:@"isExecuting"]; 269 | } 270 | 271 | - (BOOL)isConcurrent { 272 | return YES; 273 | } 274 | 275 | - (void)URLSession:(NSURLSession *)session 276 | dataTask:(NSURLSessionDataTask *)dataTask 277 | didReceiveResponse:(NSURLResponse *)response 278 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { 279 | 280 | //'304 Not Modified' is an exceptional one 281 | if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) { 282 | NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; 283 | 284 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.request.URL.absoluteString]; 285 | [receipt setTotalBytesExpectedToWrite:expected + receipt.totalBytesWritten]; 286 | receipt.date = [NSDate date]; 287 | 288 | self.expectedSize = expected; 289 | for (MCDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { 290 | progressBlock(0, expected, 0,self.request.URL); 291 | } 292 | 293 | self.response = response; 294 | dispatch_async(dispatch_get_main_queue(), ^{ 295 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadReceiveResponseNotification object:self]; 296 | }); 297 | }else if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode == 416)) { 298 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadFinishNotification object:self]; 299 | 300 | [self callCompletionBlocksWithFileURL:[NSURL fileURLWithPath:self.receipt.filePath] data:[NSData dataWithContentsOfFile:self.receipt.filePath] error:nil finished:YES]; 301 | [self done]; 302 | } 303 | else { 304 | NSUInteger code = ((NSHTTPURLResponse *)response).statusCode; 305 | 306 | //This is the case when server returns '304 Not Modified'. It means that remote image is not changed. 307 | //In case of 304 we need just cancel the operation and return cached image from the cache. 308 | if (code == 304) { 309 | [self cancelInternal]; 310 | } else { 311 | [self.dataTask cancel]; 312 | [self.receipt setState:MCDownloadStateNone]; 313 | } 314 | dispatch_async(dispatch_get_main_queue(), ^{ 315 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStopNotification object:self]; 316 | }); 317 | 318 | [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]]; 319 | [self.receipt setState:MCDownloadStateNone]; 320 | [self done]; 321 | } 322 | 323 | if (completionHandler) { 324 | completionHandler(NSURLSessionResponseAllow); 325 | } 326 | } 327 | 328 | - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { 329 | 330 | __block NSError *error = nil; 331 | MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.request.URL.absoluteString]; 332 | 333 | // Speed 334 | receipt.totalRead += data.length; 335 | NSDate *currentDate = [NSDate date]; 336 | if ([currentDate timeIntervalSinceDate:receipt.date] >= 1) { 337 | double time = [currentDate timeIntervalSinceDate:receipt.date]; 338 | long long speed = receipt.totalRead/time; 339 | receipt.speed = [self formatByteCount:speed]; 340 | receipt.totalRead = 0.0; 341 | receipt.date = currentDate; 342 | } 343 | 344 | // Write Data 345 | NSInputStream *inputStream = [[NSInputStream alloc] initWithData:data]; 346 | NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:[NSURL fileURLWithPath:receipt.filePath] append:YES]; 347 | [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 348 | [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 349 | 350 | [inputStream open]; 351 | [outputStream open]; 352 | 353 | while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) { 354 | uint8_t buffer[1024]; 355 | 356 | NSInteger bytesRead = [inputStream read:buffer maxLength:1024]; 357 | if (inputStream.streamError || bytesRead < 0) { 358 | error = inputStream.streamError; 359 | break; 360 | } 361 | 362 | NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead]; 363 | if (outputStream.streamError || bytesWritten < 0) { 364 | error = outputStream.streamError; 365 | break; 366 | } 367 | 368 | if (bytesRead == 0 && bytesWritten == 0) { 369 | break; 370 | } 371 | } 372 | [outputStream close]; 373 | [inputStream close]; 374 | 375 | receipt.progress.totalUnitCount = receipt.totalBytesExpectedToWrite; 376 | receipt.progress.completedUnitCount = receipt.totalBytesWritten; 377 | 378 | dispatch_main_async_safe(^{ 379 | for (MCDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { 380 | progressBlock(receipt.progress.completedUnitCount, receipt.progress.totalUnitCount, receipt.speed.integerValue, self.request.URL); 381 | } 382 | if (self.receipt.downloaderProgressBlock) { 383 | self.receipt.downloaderProgressBlock(receipt.progress.completedUnitCount, receipt.progress.totalUnitCount, receipt.speed.integerValue, self.request.URL); 384 | } 385 | }); 386 | } 387 | 388 | - (void)URLSession:(NSURLSession *)session 389 | dataTask:(NSURLSessionDataTask *)dataTask 390 | willCacheResponse:(NSCachedURLResponse *)proposedResponse 391 | completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { 392 | 393 | responseFromCached = NO; // If this method is called, it means the response wasn't read from cache 394 | NSCachedURLResponse *cachedResponse = proposedResponse; 395 | 396 | if (completionHandler) { 397 | completionHandler(cachedResponse); 398 | } 399 | } 400 | 401 | #pragma mark NSURLSessionTaskDelegate 402 | 403 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error { 404 | @synchronized(self) { 405 | self.dataTask = nil; 406 | dispatch_async(dispatch_get_main_queue(), ^{ 407 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadStopNotification object:self]; 408 | if (!error) { 409 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadFinishNotification object:self]; 410 | } 411 | }); 412 | } 413 | 414 | if (error) { 415 | [self callCompletionBlocksWithError:error]; 416 | } else { 417 | MCDownloadReceipt *receipt = self.receipt; 418 | [receipt setState:MCDownloadStateCompleted]; 419 | if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { 420 | 421 | [self callCompletionBlocksWithFileURL:[NSURL fileURLWithPath:receipt.filePath] data:[NSData dataWithContentsOfFile:receipt.filePath] error:nil finished:YES]; 422 | 423 | } 424 | dispatch_main_async_safe(^{ 425 | if (self.receipt.downloaderCompletedBlock) { 426 | self.receipt.downloaderCompletedBlock(receipt, nil, YES); 427 | } 428 | }); 429 | } 430 | [self done]; 431 | } 432 | 433 | 434 | 435 | 436 | - (BOOL)shouldContinueWhenAppEntersBackground { 437 | return YES; 438 | } 439 | 440 | - (void)callCompletionBlocksWithError:(nullable NSError *)error { 441 | [self callCompletionBlocksWithFileURL:nil data:nil error:error finished:YES]; 442 | } 443 | 444 | - (void)callCompletionBlocksWithFileURL:(nullable NSURL *)fileURL 445 | data:(nullable NSData *)data 446 | error:(nullable NSError *)error 447 | finished:(BOOL)finished { 448 | 449 | if (error) { 450 | [self.receipt setState:MCDownloadStateFailed]; 451 | }else { 452 | [self.receipt setState:MCDownloadStateCompleted]; 453 | } 454 | NSArray *completionBlocks = [self callbacksForKey:kCompletedCallbackKey]; 455 | dispatch_main_async_safe(^{ 456 | for (MCDownloaderCompletedBlock completedBlock in completionBlocks) { 457 | completedBlock(self.receipt, error, finished); 458 | } 459 | 460 | if (self.receipt.downloaderCompletedBlock) { 461 | self.receipt.downloaderCompletedBlock(self.receipt, error, YES); 462 | } 463 | }); 464 | } 465 | 466 | - (NSString*)formatByteCount:(long long)size 467 | { 468 | return [NSByteCountFormatter stringFromByteCount:size countStyle:NSByteCountFormatterCountStyleFile]; 469 | } 470 | @end 471 | 472 | 473 | NS_ASSUME_NONNULL_END 474 | -------------------------------------------------------------------------------- /Source/MCDownloadReceipt.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloadReceipt.h 3 | // MCDownloadManager 4 | // 5 | // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com) 6 | // Copyright © 2017年 qikeyun. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | @class MCDownloadReceipt; 30 | 31 | /** The download state */ 32 | typedef NS_ENUM(NSUInteger, MCDownloadState) { 33 | MCDownloadStateNone, /** default */ 34 | MCDownloadStateWillResume, /** waiting */ 35 | MCDownloadStateDownloading, /** downloading */ 36 | MCDownloadStateSuspened, /** suspened */ 37 | MCDownloadStateCompleted, /** download completed */ 38 | MCDownloadStateFailed /** download failed */ 39 | }; 40 | 41 | /** The download prioritization */ 42 | typedef NS_ENUM(NSInteger, MCDownloadPrioritization) { 43 | MCDownloadPrioritizationFIFO, /** first in first out */ 44 | MCDownloadPrioritizationLIFO /** last in first out */ 45 | }; 46 | 47 | 48 | typedef void (^MCDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL); 49 | typedef void (^MCDownloaderCompletedBlock)(MCDownloadReceipt * _Nullable receipt, NSError * _Nullable error, BOOL finished); 50 | typedef NSString *_Nullable (^MCDownloaderReceiptCustomFilePathBlock)(MCDownloadReceipt * _Nullable receipt); 51 | 52 | /** 53 | * The receipt of a downloader,we can get all the informations from the receipt. 54 | */ 55 | @interface MCDownloadReceipt : NSObject 56 | 57 | /** 58 | * Download State 59 | */ 60 | @property (nonatomic, assign, readonly) MCDownloadState state; 61 | 62 | /** 63 | The download source url 64 | */ 65 | @property (nonatomic, copy, readonly, nonnull) NSString *url; 66 | 67 | /** 68 | The file path, you can use it to get the downloaded data. 69 | */ 70 | @property (nonatomic, copy, readonly, nonnull) NSString *filePath; 71 | 72 | /** 73 | The url's pathExtension through the MD5 processing. 74 | */ 75 | @property (nonatomic, copy, readonly, nullable) NSString *filename; 76 | 77 | /** 78 | The url's pathExtension without through the MD5 processing. 79 | */ 80 | @property (nonatomic, copy, readonly, nullable) NSString *truename; 81 | 82 | /** 83 | The url's pathExtension without through the MD5 processing. 84 | */ 85 | @property (nonatomic, copy, nullable) MCDownloaderReceiptCustomFilePathBlock customFilePathBlock; 86 | 87 | /** 88 | The current download speed, 89 | */ 90 | @property (nonatomic, copy, readonly, nullable) NSString *speed; // KB/s 91 | 92 | @property (assign, nonatomic, readonly) long long totalBytesWritten; 93 | @property (assign, nonatomic, readonly) long long totalBytesExpectedToWrite; 94 | 95 | /** 96 | The current download progress object 97 | */ 98 | @property (nonatomic, strong, readonly, nullable) NSProgress *progress; 99 | 100 | @property (nonatomic, strong, readonly, nullable) NSError *error; 101 | 102 | 103 | /** 104 | The call back block. When setting this block,the progress block will be called during downloading,the complete block will be called after download finished. 105 | */ 106 | @property (nonatomic,copy, nullable, readonly)MCDownloaderProgressBlock downloaderProgressBlock; 107 | @property (nonatomic,copy, nullable, readonly)MCDownloaderCompletedBlock downloaderCompletedBlock; 108 | 109 | 110 | 111 | 112 | 113 | #pragma mark - Private Methods 114 | ///============================================================================= 115 | /// Method is at the bottom of the private method, do not need to use 116 | ///============================================================================= 117 | 118 | /** 119 | The `MCDowmloadReceipt` method of initialization. Generally don't need to use this method. 120 | 121 | use `MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:url];` to get the `MCDowmloadReceipt`. 122 | 123 | */ 124 | - (nonnull instancetype)initWithURLString:(nonnull NSString *)URLString 125 | downloadOperationCancelToken:(nullable id)downloadOperationCancelToken 126 | downloaderProgressBlock:(nullable MCDownloaderProgressBlock)downloaderProgressBlock 127 | downloaderCompletedBlock:(nullable MCDownloaderCompletedBlock)downloaderCompletedBlock; 128 | 129 | - (void)setTotalBytesExpectedToWrite:(long long)totalBytesExpectedToWrite; 130 | - (void)setState:(MCDownloadState)state; 131 | - (void)setDownloadOperationCancelToken:(nullable id)downloadOperationCancelToken; 132 | - (void)setDownloaderProgressBlock:(nullable MCDownloaderProgressBlock)downloaderProgressBlock; 133 | - (void)setDownloaderCompletedBlock:(nullable MCDownloaderCompletedBlock)downloaderCompletedBlock; 134 | - (void)setSpeed:(NSString * _Nullable)speed; 135 | 136 | 137 | 138 | /** 139 | Auxiliary attributes and don't need to use 140 | */ 141 | @property (nonatomic, assign) NSUInteger totalRead; 142 | @property (nonatomic, strong, nullable) NSDate *date; 143 | @property (nonatomic, strong, nullable) id downloadOperationCancelToken; 144 | @end 145 | -------------------------------------------------------------------------------- /Source/MCDownloadReceipt.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloadReceipt.m 3 | // MCDownloadManager 4 | // 5 | // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com) 6 | // Copyright © 2017年 qikeyun. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import "MCDownloadReceipt.h" 28 | #import 29 | 30 | extern NSString * cacheFolder(); 31 | 32 | static unsigned long long fileSizeForPath(NSString *path) { 33 | 34 | signed long long fileSize = 0; 35 | NSFileManager *fileManager = [NSFileManager defaultManager]; 36 | if ([fileManager fileExistsAtPath:path]) { 37 | NSError *error = nil; 38 | NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error]; 39 | if (!error && fileDict) { 40 | fileSize = [fileDict fileSize]; 41 | } 42 | } 43 | return fileSize; 44 | } 45 | 46 | static NSString * getMD5String(NSString *str) { 47 | 48 | if (str == nil) return nil; 49 | 50 | const char *cstring = str.UTF8String; 51 | unsigned char bytes[CC_MD5_DIGEST_LENGTH]; 52 | CC_MD5(cstring, (CC_LONG)strlen(cstring), bytes); 53 | 54 | NSMutableString *md5String = [NSMutableString string]; 55 | for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { 56 | [md5String appendFormat:@"%02x", bytes[i]]; 57 | } 58 | return md5String; 59 | } 60 | 61 | @interface MCDownloadReceipt() 62 | 63 | @property (nonatomic, assign) MCDownloadState state; 64 | 65 | @property (nonatomic, copy) NSString *url; 66 | @property (nonatomic, copy) NSString *filePath; 67 | @property (nonatomic, copy) NSString *filename; 68 | @property (nonatomic, copy) NSString *truename; 69 | @property (nonatomic, strong) NSProgress *progress; 70 | 71 | @property (assign, nonatomic) long long totalBytesWritten; 72 | 73 | @end 74 | 75 | @implementation MCDownloadReceipt 76 | 77 | - (NSString *)filePath { 78 | if (_filePath == nil) { 79 | NSString *path = [cacheFolder() stringByAppendingPathComponent:self.filename]; 80 | if (![path isEqualToString:_filePath] ) { 81 | if (_filePath && ![[NSFileManager defaultManager] fileExistsAtPath:_filePath]) { 82 | NSString *dir = [_filePath stringByDeletingLastPathComponent]; 83 | [[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; 84 | } 85 | _filePath = path; 86 | } 87 | } 88 | return _filePath; 89 | } 90 | 91 | - (void)setCustomFilePathBlock:(nullable MCDownloaderReceiptCustomFilePathBlock)customFilePathBlock { 92 | _customFilePathBlock = customFilePathBlock; 93 | if (_customFilePathBlock) { 94 | NSString *path = customFilePathBlock(self); 95 | if (path && ![path isEqualToString:_filePath] ) { 96 | _filePath = path; 97 | if (_filePath && ![[NSFileManager defaultManager] fileExistsAtPath:_filePath]) { 98 | NSString *dir = [_filePath stringByDeletingLastPathComponent]; 99 | [[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; 100 | } 101 | } 102 | } 103 | } 104 | 105 | - (NSString *)filename { 106 | if (_filename == nil) { 107 | NSString *pathExtension = self.url.pathExtension; 108 | if (pathExtension.length) { 109 | _filename = [NSString stringWithFormat:@"%@.%@", getMD5String(self.url), pathExtension]; 110 | } else { 111 | _filename = getMD5String(self.url); 112 | } 113 | } 114 | return _filename; 115 | } 116 | 117 | - (NSString *)truename { 118 | if (_truename == nil) { 119 | _truename = self.url.lastPathComponent; 120 | } 121 | return _truename; 122 | } 123 | 124 | - (NSProgress *)progress { 125 | if (_progress == nil) { 126 | _progress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; 127 | } 128 | @try { 129 | _progress.totalUnitCount = self.totalBytesExpectedToWrite; 130 | _progress.completedUnitCount = self.totalBytesWritten; 131 | } @catch (NSException *exception) { 132 | 133 | } 134 | return _progress; 135 | } 136 | 137 | - (long long)totalBytesWritten { 138 | 139 | return fileSizeForPath(self.filePath); 140 | } 141 | 142 | 143 | - (instancetype)initWithURL:(NSString *)url { 144 | if (self = [self init]) { 145 | 146 | self.url = url; 147 | } 148 | return self; 149 | } 150 | 151 | #pragma mark - NSCoding 152 | - (void)encodeWithCoder:(NSCoder *)aCoder 153 | { 154 | [aCoder encodeObject:self.url forKey:NSStringFromSelector(@selector(url))]; 155 | [aCoder encodeObject:self.filePath forKey:NSStringFromSelector(@selector(filePath))]; 156 | [aCoder encodeObject:@(self.state) forKey:NSStringFromSelector(@selector(state))]; 157 | [aCoder encodeObject:self.filename forKey:NSStringFromSelector(@selector(filename))]; 158 | [aCoder encodeObject:@(self.totalBytesWritten) forKey:NSStringFromSelector(@selector(totalBytesWritten))]; 159 | [aCoder encodeObject:@(self.totalBytesExpectedToWrite) forKey:NSStringFromSelector(@selector(totalBytesExpectedToWrite))]; 160 | 161 | } 162 | 163 | - (id)initWithCoder:(NSCoder *)aDecoder 164 | { 165 | self = [super init]; 166 | if (self) { 167 | self.url = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(url))]; 168 | self.filePath = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(filePath))]; 169 | self.state = [[aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(state))] unsignedIntegerValue]; 170 | self.filename = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(filename))]; 171 | self.totalBytesWritten = [[aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(totalBytesWritten))] unsignedIntegerValue]; 172 | self.totalBytesExpectedToWrite = [[aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(totalBytesExpectedToWrite))] unsignedIntegerValue]; 173 | 174 | } 175 | return self; 176 | } 177 | 178 | 179 | - (instancetype)initWithURLString:(NSString *)URLString 180 | downloadOperationCancelToken:(id)downloadOperationCancelToken 181 | downloaderProgressBlock:(MCDownloaderProgressBlock)downloaderProgressBlock 182 | downloaderCompletedBlock:(MCDownloaderCompletedBlock)downloaderCompletedBlock { 183 | 184 | if (self = [self init]) { 185 | 186 | self.url = URLString; 187 | self.totalBytesExpectedToWrite = 0; 188 | self.downloadOperationCancelToken = downloadOperationCancelToken; 189 | self.downloaderProgressBlock = downloaderProgressBlock; 190 | self.downloaderCompletedBlock = downloaderCompletedBlock; 191 | } 192 | return self; 193 | } 194 | 195 | - (void)setTotalBytesExpectedToWrite:(long long)totalBytesExpectedToWrite { 196 | _totalBytesExpectedToWrite = totalBytesExpectedToWrite; 197 | } 198 | 199 | - (void)setState:(MCDownloadState)state { 200 | _state = state; 201 | } 202 | 203 | - (void)setDownloadOperationCancelToken:(id)downloadOperationCancelToken { 204 | _downloadOperationCancelToken = downloadOperationCancelToken; 205 | } 206 | 207 | - (void)setDownloaderProgressBlock:(MCDownloaderProgressBlock)downloaderProgressBlock { 208 | _downloaderProgressBlock = downloaderProgressBlock; 209 | } 210 | 211 | - (void)setDownloaderCompletedBlock:(MCDownloaderCompletedBlock)downloaderCompletedBlock { 212 | _downloaderCompletedBlock = downloaderCompletedBlock; 213 | } 214 | 215 | - (void)setSpeed:(NSString *)speed { 216 | _speed = speed; 217 | } 218 | 219 | 220 | 221 | @end 222 | -------------------------------------------------------------------------------- /Source/MCDownloader.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloader.h 3 | // MCDownloadManager 4 | // 5 | // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com) 6 | // Copyright © 2017年 qikeyun. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | #import 27 | #import "MCDownloadReceipt.h" 28 | #import 29 | 30 | NS_ASSUME_NONNULL_BEGIN 31 | 32 | // Use dispatch_main_async_safe instead of dispatch_async(dispatch_get_main_queue(), block) 33 | #ifndef dispatch_main_async_safe 34 | #define dispatch_main_async_safe(block)\ 35 | if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\ 36 | block();\ 37 | } else {\ 38 | dispatch_async(dispatch_get_main_queue(), block);\ 39 | } 40 | #endif 41 | 42 | 43 | 44 | FOUNDATION_EXPORT NSString * const MCDownloadCacheFolderName; 45 | FOUNDATION_EXPORT NSString * cacheFolder(); 46 | 47 | 48 | extern NSString * _Nonnull const MCDownloadStartNotification; 49 | extern NSString * _Nonnull const MCDownloadStopNotification; 50 | 51 | typedef NSDictionary MCHTTPHeadersDictionary; 52 | typedef NSMutableDictionary MCHTTPHeadersMutableDictionary; 53 | 54 | typedef MCHTTPHeadersDictionary * _Nullable (^MCDownloaderHeadersFilterBlock)(NSURL * _Nullable url, MCHTTPHeadersDictionary * _Nullable headers); 55 | 56 | 57 | @interface MCDownloader : NSObject 58 | 59 | /** 60 | * The maximum number of concurrent downloads 61 | */ 62 | @property (assign, nonatomic) NSInteger maxConcurrentDownloads; 63 | 64 | /** 65 | * Shows the current amount of downloads that still need to be downloaded 66 | */ 67 | @property (readonly, nonatomic) NSUInteger currentDownloadCount; 68 | 69 | 70 | /** 71 | * The timeout value (in seconds) for the download operation. Default: 15.0. 72 | */ 73 | @property (assign, nonatomic) NSTimeInterval downloadTimeout; 74 | 75 | 76 | /** 77 | Defines the order prioritization of incoming download requests being inserted into the queue. `MCDownloadPrioritizationFIFO` by default. 78 | */ 79 | @property (nonatomic, assign) MCDownloadPrioritization downloadPrioritizaton; 80 | /** 81 | * Singleton method, returns the shared instance 82 | * 83 | * @return global shared instance of downloader class 84 | */ 85 | + (nonnull instancetype)sharedDownloader; 86 | 87 | 88 | /** 89 | * Set filter to pick headers for downloading image HTTP request. 90 | * 91 | * This block will be invoked for each downloading image request, returned 92 | * NSDictionary will be used as headers in corresponding HTTP request. 93 | */ 94 | @property (nonatomic, copy, nullable) MCDownloaderHeadersFilterBlock headersFilter; 95 | 96 | /** 97 | * Creates an instance of a downloader with specified session configuration. 98 | * *Note*: `timeoutIntervalForRequest` is going to be overwritten. 99 | * @return new instance of downloader class 100 | */ 101 | - (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER; 102 | 103 | /** 104 | * Set a value for a HTTP header to be appended to each download HTTP request. 105 | * 106 | * @param value The value for the header field. Use `nil` value to remove the header. 107 | * @param field The name of the header field to set. 108 | */ 109 | - (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field; 110 | 111 | /** 112 | * Returns the value of the specified HTTP header field. 113 | * 114 | * @return The value associated with the header field field, or `nil` if there is no corresponding header field. 115 | */ 116 | - (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field; 117 | 118 | /** 119 | * Sets a subclass of `MCDownloadOperation` as the default 120 | * `NSOperation` to be used each time MCDownload constructs a request 121 | * operation to download an data. 122 | * 123 | * @param operationClass The subclass of `MCDownloadOperation` to set 124 | * as default. Passing `nil` will revert to `MCDownloadOperation`. 125 | */ 126 | - (void)setOperationClass:(nullable Class)operationClass; 127 | 128 | /** 129 | Creates an `MCDownloadReceipt` with the specified request. 130 | 131 | @param url The URL for the request. 132 | @param progressBlock A block object to be executed when the download progress is updated. Note this block is called on the main queue. 133 | */ 134 | - (nullable MCDownloadReceipt *)downloadDataWithURL:(nullable NSURL *)url 135 | progress:(nullable MCDownloaderProgressBlock)progressBlock 136 | completed:(nullable MCDownloaderCompletedBlock)completedBlock; 137 | 138 | - (nullable MCDownloadReceipt *)downloadReceiptForURLString:(nullable NSString *)URLString; 139 | 140 | - (void)cancel:(nullable MCDownloadReceipt *)token completed:(nullable void (^)())completed; 141 | 142 | - (void)remove:(nullable MCDownloadReceipt *)token completed:(nullable void (^)())completed; 143 | /** 144 | * Sets the download queue suspension state 145 | */ 146 | - (void)setSuspended:(BOOL)suspended; 147 | 148 | /** 149 | * Cancels all download operations in the queue 150 | */ 151 | - (void)cancelAllDownloads; 152 | 153 | /** 154 | Romove All files in the cache folder. 155 | @Waring: 156 | This method is synchronized methods, you should be careful when using, will delete all the data in the cache folder 157 | */ 158 | - (void)removeAndClearAll; 159 | @end 160 | 161 | NS_ASSUME_NONNULL_END 162 | -------------------------------------------------------------------------------- /Source/MCDownloader.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCDownloader.m 3 | // MCDownloadManager 4 | // 5 | // Created by M.C on 17/4/6. (QQ:714080794 Gmail:chaoma0609@gmail.com) 6 | // Copyright © 2017年 qikeyun. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import "MCDownloader.h" 28 | #import "MCDownloadOperation.h" 29 | #import "MCDownloadReceipt.h" 30 | 31 | NSString * const MCDownloadCacheFolderName = @"MCDownloadCache"; 32 | 33 | static NSString *cacheFolderPath; 34 | 35 | NSString * cacheFolder() { 36 | if (!cacheFolderPath) { 37 | NSString *cacheDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject; 38 | cacheFolderPath = [cacheDir stringByAppendingPathComponent:MCDownloadCacheFolderName]; 39 | NSFileManager *filemgr = [NSFileManager defaultManager]; 40 | NSError *error = nil; 41 | if(![filemgr createDirectoryAtPath:cacheFolderPath withIntermediateDirectories:YES attributes:nil error:&error]) { 42 | NSLog(@"Failed to create cache directory at %@", cacheFolderPath); 43 | cacheFolderPath = nil; 44 | } 45 | } 46 | return cacheFolderPath; 47 | } 48 | 49 | static void clearCacheFolder() { 50 | cacheFolderPath = nil; 51 | } 52 | 53 | static NSString * LocalReceiptsPath() { 54 | return [cacheFolder() stringByAppendingPathComponent:@"receipts.data"]; 55 | } 56 | 57 | @interface MCDownloader() 58 | 59 | @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue; 60 | @property (weak, nonatomic, nullable) NSOperation *lastAddedOperation; 61 | @property (assign, nonatomic, nullable) Class operationClass; 62 | @property (strong, nonatomic, nonnull) NSMutableDictionary *URLOperations; 63 | @property (strong, nonatomic, nullable) MCHTTPHeadersMutableDictionary *HTTPHeaders; 64 | // This queue is used to serialize the handling of the network responses of all the download operation in a single queue 65 | @property (strong, nonatomic, nullable) dispatch_queue_t barrierQueue; 66 | 67 | // The session in which data tasks will run 68 | @property (strong, nonatomic) NSURLSession *session; 69 | 70 | @property (nonatomic, strong) NSMutableDictionary *allDownloadReceipts; 71 | @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; 72 | @end 73 | @implementation MCDownloader 74 | 75 | - (NSMutableDictionary *)allDownloadReceipts { 76 | if (_allDownloadReceipts == nil) { 77 | NSDictionary *receipts = [NSKeyedUnarchiver unarchiveObjectWithFile:LocalReceiptsPath()]; 78 | _allDownloadReceipts = receipts != nil ? receipts.mutableCopy : [NSMutableDictionary dictionary]; 79 | } 80 | return _allDownloadReceipts; 81 | } 82 | 83 | + (nonnull instancetype)sharedDownloader { 84 | static dispatch_once_t once; 85 | static id instance; 86 | dispatch_once(&once, ^{ 87 | instance = [self new]; 88 | }); 89 | return instance; 90 | } 91 | 92 | - (nonnull instancetype)init { 93 | return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; 94 | } 95 | 96 | - (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration { 97 | if ((self = [super init])) { 98 | _operationClass = [MCDownloadOperation class]; 99 | _downloadPrioritizaton = MCDownloadPrioritizationFIFO; 100 | _downloadQueue = [NSOperationQueue new]; 101 | _downloadQueue.maxConcurrentOperationCount = 3; 102 | _downloadQueue.name = @"com.machao.MCDownloader"; 103 | _URLOperations = [NSMutableDictionary new]; 104 | _barrierQueue = dispatch_queue_create("com.machao.MCDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); 105 | _downloadTimeout = 15.0; 106 | 107 | sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout; 108 | sessionConfiguration.HTTPMaximumConnectionsPerHost = 10; 109 | /** 110 | * Create the session for this task 111 | * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate 112 | * method calls and completion handler calls. 113 | */ 114 | self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration 115 | delegate:self 116 | delegateQueue:nil]; 117 | 118 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; 119 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 120 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; 121 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; 122 | } 123 | return self; 124 | } 125 | 126 | #pragma mark - NSNotification 127 | - (void)applicationWillTerminate:(NSNotification *)not { 128 | [self setAllStateToNone]; 129 | [self saveAllDownloadReceipts]; 130 | } 131 | 132 | - (void)applicationDidReceiveMemoryWarning:(NSNotification *)not { 133 | [self saveAllDownloadReceipts]; 134 | } 135 | 136 | - (void)applicationWillResignActive:(NSNotification *)not { 137 | [self saveAllDownloadReceipts]; 138 | /// 捕获到失去激活状态后 139 | Class UIApplicationClass = NSClassFromString(@"UIApplication"); 140 | BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; 141 | if (hasApplication ) { 142 | __weak __typeof__ (self) wself = self; 143 | UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; 144 | self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ 145 | __strong __typeof (wself) sself = wself; 146 | 147 | if (sself) { 148 | [sself setAllStateToNone]; 149 | [sself saveAllDownloadReceipts]; 150 | 151 | [app endBackgroundTask:sself.backgroundTaskId]; 152 | sself.backgroundTaskId = UIBackgroundTaskInvalid; 153 | } 154 | }]; 155 | } 156 | } 157 | 158 | - (void)applicationDidBecomeActive:(NSNotification *)not { 159 | 160 | Class UIApplicationClass = NSClassFromString(@"UIApplication"); 161 | if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { 162 | return; 163 | } 164 | if (self.backgroundTaskId != UIBackgroundTaskInvalid) { 165 | UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; 166 | [app endBackgroundTask:self.backgroundTaskId]; 167 | self.backgroundTaskId = UIBackgroundTaskInvalid; 168 | } 169 | 170 | NSString *cacheDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject; 171 | NSString *cachePath = [cacheDir stringByAppendingPathComponent:MCDownloadCacheFolderName]; 172 | NSString *existedCacheFolderPath = cacheFolder(); 173 | if (existedCacheFolderPath && ![existedCacheFolderPath isEqualToString:cachePath]) { 174 | clearCacheFolder(); 175 | [self.allDownloadReceipts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 176 | if ([obj isKindOfClass:[MCDownloadReceipt class]]) { 177 | MCDownloadReceipt *receipt = obj; 178 | [receipt setValue:nil forKey:@"filePath"]; 179 | } 180 | }]; 181 | } 182 | } 183 | 184 | - (void)setAllStateToNone { 185 | [self.allDownloadReceipts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 186 | if ([obj isKindOfClass:[MCDownloadReceipt class]]) { 187 | MCDownloadReceipt *receipt = obj; 188 | if (receipt.state != MCDownloadStateCompleted) { 189 | [receipt setState:MCDownloadStateNone]; 190 | } 191 | } 192 | }]; 193 | } 194 | 195 | - (void)saveAllDownloadReceipts { 196 | [NSKeyedArchiver archiveRootObject:self.allDownloadReceipts toFile:LocalReceiptsPath()]; 197 | } 198 | 199 | - (void)dealloc { 200 | [self.session invalidateAndCancel]; 201 | self.session = nil; 202 | 203 | [self.downloadQueue cancelAllOperations]; 204 | 205 | } 206 | 207 | - (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field { 208 | if (value) { 209 | self.HTTPHeaders[field] = value; 210 | } 211 | else { 212 | [self.HTTPHeaders removeObjectForKey:field]; 213 | } 214 | } 215 | 216 | - (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field { 217 | return self.HTTPHeaders[field]; 218 | } 219 | 220 | - (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads { 221 | _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads; 222 | } 223 | 224 | - (NSUInteger)currentDownloadCount { 225 | return _downloadQueue.operationCount; 226 | } 227 | 228 | - (NSInteger)maxConcurrentDownloads { 229 | return _downloadQueue.maxConcurrentOperationCount; 230 | } 231 | 232 | - (void)setOperationClass:(nullable Class)operationClass { 233 | if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(MCDownloaderOperationInterface)]) { 234 | _operationClass = operationClass; 235 | } else { 236 | _operationClass = [MCDownloadOperation class]; 237 | } 238 | } 239 | 240 | - (nullable MCDownloadReceipt *)downloadDataWithURL:(nullable NSURL *)url 241 | progress:(nullable MCDownloaderProgressBlock)progressBlock 242 | completed:(nullable MCDownloaderCompletedBlock)completedBlock { 243 | __weak MCDownloader *wself = self; 244 | 245 | MCDownloadReceipt *receipt = [self downloadReceiptForURLString:url.absoluteString]; 246 | if (receipt.state == MCDownloadStateCompleted) { 247 | dispatch_main_async_safe(^{ 248 | 249 | [[NSNotificationCenter defaultCenter] postNotificationName:MCDownloadFinishNotification object:self]; 250 | if (completedBlock) { 251 | completedBlock(receipt ,nil ,YES); 252 | } 253 | if (receipt.downloaderCompletedBlock) { 254 | receipt.downloaderCompletedBlock(receipt, nil, YES); 255 | } 256 | 257 | }); 258 | return receipt; 259 | } 260 | 261 | return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^MCDownloadOperation *{ 262 | __strong __typeof (wself) sself = wself; 263 | 264 | NSTimeInterval timeoutInterval = sself.downloadTimeout; 265 | if (timeoutInterval == 0.0) { 266 | timeoutInterval = 15.0; 267 | } 268 | 269 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; 270 | MCDownloadReceipt *receipt = [sself downloadReceiptForURLString:url.absoluteString]; 271 | if (receipt.totalBytesWritten > 0) { 272 | NSString *range = [NSString stringWithFormat:@"bytes=%zd-", receipt.totalBytesWritten]; 273 | [request setValue:range forHTTPHeaderField:@"Range"]; 274 | } 275 | 276 | request.HTTPShouldUsePipelining = YES; 277 | 278 | if (sself.headersFilter) { 279 | request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]); 280 | } 281 | else { 282 | request.allHTTPHeaderFields = sself.HTTPHeaders; 283 | } 284 | 285 | 286 | MCDownloadOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session]; 287 | 288 | [sself.downloadQueue addOperation:operation]; 289 | 290 | 291 | if (sself.downloadPrioritizaton == MCDownloadPrioritizationLIFO) { 292 | // Emulate LIFO execution order by systematically adding new operations as last operation's dependency 293 | [sself.lastAddedOperation addDependency:operation]; 294 | sself.lastAddedOperation = operation; 295 | } 296 | 297 | return operation; 298 | }]; 299 | } 300 | 301 | - (MCDownloadReceipt *)downloadReceiptForURLString:(NSString *)URLString { 302 | if (URLString == nil) { 303 | return nil; 304 | } 305 | if (self.allDownloadReceipts[URLString]) { 306 | return self.allDownloadReceipts[URLString]; 307 | }else { 308 | MCDownloadReceipt *receipt = [[MCDownloadReceipt alloc] initWithURLString:URLString downloadOperationCancelToken:nil downloaderProgressBlock:nil downloaderCompletedBlock:nil]; 309 | self.allDownloadReceipts[URLString] = receipt; 310 | return receipt; 311 | } 312 | 313 | return nil; 314 | } 315 | 316 | 317 | 318 | - (nullable MCDownloadReceipt *)addProgressCallback:(MCDownloaderProgressBlock)progressBlock 319 | completedBlock:(MCDownloaderCompletedBlock)completedBlock 320 | forURL:(nullable NSURL *)url 321 | createCallback:(MCDownloadOperation *(^)())createCallback { 322 | // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. 323 | if (url == nil) { 324 | if (completedBlock != nil) { 325 | completedBlock(nil, nil, NO); 326 | } 327 | return nil; 328 | } 329 | 330 | __block MCDownloadReceipt *token = nil; 331 | 332 | dispatch_barrier_sync(self.barrierQueue, ^{ 333 | MCDownloadOperation *operation = self.URLOperations[url]; 334 | if (!operation) { 335 | operation = createCallback(); 336 | self.URLOperations[url] = operation; 337 | 338 | __weak MCDownloadOperation *woperation = operation; 339 | operation.completionBlock = ^{ 340 | MCDownloadOperation *soperation = woperation; 341 | if (!soperation) return; 342 | if (self.URLOperations[url] == soperation) { 343 | [self.URLOperations removeObjectForKey:url]; 344 | }; 345 | }; 346 | } 347 | id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; 348 | 349 | if (!self.allDownloadReceipts[url.absoluteString]) { 350 | token = [[MCDownloadReceipt alloc] initWithURLString:url.absoluteString 351 | downloadOperationCancelToken:downloadOperationCancelToken 352 | downloaderProgressBlock:nil 353 | downloaderCompletedBlock:nil]; 354 | self.allDownloadReceipts[url.absoluteString] = token; 355 | }else { 356 | token = self.allDownloadReceipts[url.absoluteString]; 357 | 358 | if (!token.downloadOperationCancelToken) { 359 | [token setDownloadOperationCancelToken:downloadOperationCancelToken]; 360 | } 361 | } 362 | 363 | }); 364 | 365 | return token; 366 | } 367 | 368 | #pragma mark - Control Methods 369 | 370 | - (void)cancel:(nullable MCDownloadReceipt *)token completed:(nullable void (^)())completed { 371 | dispatch_barrier_async(self.barrierQueue, ^{ 372 | MCDownloadOperation *operation = self.URLOperations[[NSURL URLWithString:token.url]]; 373 | BOOL canceled = [operation cancel:token.downloadOperationCancelToken]; 374 | if (canceled) { 375 | [self.URLOperations removeObjectForKey:[NSURL URLWithString:token.url]]; 376 | [token setState:MCDownloadStateNone]; 377 | // [self.allDownloadReceipts removeObjectForKey:token.url]; 378 | 379 | } 380 | dispatch_main_async_safe(^{ 381 | if (completed) { 382 | completed(); 383 | } 384 | }); 385 | }); 386 | } 387 | 388 | - (void)remove:(MCDownloadReceipt *)token completed:(nullable void (^)())completed{ 389 | [token setState:MCDownloadStateNone]; 390 | [self cancel:token completed:^{ 391 | NSFileManager *fileManager = [NSFileManager defaultManager]; 392 | 393 | [fileManager removeItemAtPath:token.filePath error:nil]; 394 | 395 | dispatch_main_async_safe(^{ 396 | if (completed) { 397 | completed(); 398 | } 399 | }); 400 | }]; 401 | 402 | } 403 | 404 | - (void)setSuspended:(BOOL)suspended { 405 | (self.downloadQueue).suspended = suspended; 406 | } 407 | 408 | - (void)cancelAllDownloads { 409 | [self.downloadQueue cancelAllOperations]; 410 | [self setAllStateToNone]; 411 | [self saveAllDownloadReceipts]; 412 | } 413 | 414 | - (void)removeAndClearAll { 415 | [self cancelAllDownloads]; 416 | NSFileManager *fileManager = [NSFileManager defaultManager]; 417 | [fileManager removeItemAtPath:cacheFolder() error:nil]; 418 | clearCacheFolder(); 419 | } 420 | 421 | #pragma mark Helper methods 422 | 423 | - (MCDownloadOperation *)operationWithTask:(NSURLSessionTask *)task { 424 | MCDownloadOperation *returnOperation = nil; 425 | for (MCDownloadOperation *operation in self.downloadQueue.operations) { 426 | if (operation.dataTask.taskIdentifier == task.taskIdentifier) { 427 | returnOperation = operation; 428 | break; 429 | } 430 | } 431 | return returnOperation; 432 | } 433 | 434 | #pragma mark NSURLSessionDataDelegate 435 | 436 | - (void)URLSession:(NSURLSession *)session 437 | dataTask:(NSURLSessionDataTask *)dataTask 438 | didReceiveResponse:(NSURLResponse *)response 439 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { 440 | 441 | // Identify the operation that runs this task and pass it the delegate method 442 | MCDownloadOperation *dataOperation = [self operationWithTask:dataTask]; 443 | 444 | [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler]; 445 | } 446 | 447 | - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { 448 | 449 | // Identify the operation that runs this task and pass it the delegate method 450 | MCDownloadOperation *dataOperation = [self operationWithTask:dataTask]; 451 | 452 | [dataOperation URLSession:session dataTask:dataTask didReceiveData:data]; 453 | } 454 | 455 | - (void)URLSession:(NSURLSession *)session 456 | dataTask:(NSURLSessionDataTask *)dataTask 457 | willCacheResponse:(NSCachedURLResponse *)proposedResponse 458 | completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { 459 | 460 | // Identify the operation that runs this task and pass it the delegate method 461 | MCDownloadOperation *dataOperation = [self operationWithTask:dataTask]; 462 | 463 | [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler]; 464 | } 465 | 466 | #pragma mark NSURLSessionTaskDelegate 467 | 468 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { 469 | // Identify the operation that runs this task and pass it the delegate method 470 | MCDownloadOperation *dataOperation = [self operationWithTask:task]; 471 | 472 | [dataOperation URLSession:session task:task didCompleteWithError:error]; 473 | } 474 | 475 | 476 | @end 477 | --------------------------------------------------------------------------------