├── .gitignore ├── Disk.podspec ├── Disk.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Disk.xcscheme ├── Disk.xcworkspace └── contents.xcworkspacedata ├── DiskExample ├── DiskExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── DiskExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Post.swift │ └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Disk+Codable.swift ├── Disk+Data.swift ├── Disk+Errors.swift ├── Disk+Helpers.swift ├── Disk+InternalHelpers.swift ├── Disk+UIImage.swift ├── Disk+VolumeInformation.swift ├── Disk+[Data].swift ├── Disk+[UIImage].swift ├── Disk.h ├── Disk.swift ├── Info.plist └── PrivacyInfo.xcprivacy └── Tests ├── DiskTests.swift ├── Images.xcassets ├── AllMight.imageset │ ├── AllMight.png │ └── Contents.json ├── Bakugo.imageset │ ├── Bakugo.png │ └── Contents.json ├── Contents.json └── Deku.imageset │ ├── Contents.json │ └── Deku 20-58-03-912.png ├── Info.plist ├── Message.swift └── UIImage+Extension.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS X Finder 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | 18 | ## Other 19 | xcuserdata 20 | *.xccheckout 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcbaseline 24 | *.xcbkptlist 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | # Swift Package Manager 31 | .build/ 32 | -------------------------------------------------------------------------------- /Disk.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Disk" 3 | s.version = "0.6.4" 4 | s.summary = "Delightful framework for iOS to easily persist structs, images, and data" 5 | s.description = <<-DESC 6 | Easily work with the iOS file system without worrying about any of its intricacies. Save Codable structs, UIImage, [UIImage], Data, [Data] to Apple recommended locations on the user's disk. Retrieve an object from disk as the type you specify, without having to worry about conversion or casting. Append data to file locations without worrying about retrieval, manipulation, or conversion. Clear entire directories if you need to, check if an object exists on disk, and much more. 7 | DESC 8 | s.homepage = "https://github.com/saoudrizwan/Disk" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.author = { "Saoud Rizwan" => "hello@saoudmr.com" } 11 | s.social_media_url = "https://twitter.com/sdrzn" 12 | s.swift_version = "4.0", "4.2", "5.0" 13 | s.platform = :ios, "9.0" 14 | s.source = { :git => "https://github.com/saoudrizwan/Disk.git", :tag => "#{s.version}" } 15 | s.source_files = "Sources/**/*.{h,m,swift}" 16 | s.resource_bundles = {"Disk" => ["Sources/PrivacyInfo.xcprivacy"]} 17 | end 18 | -------------------------------------------------------------------------------- /Disk.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 103FA3771F713332004C600F /* Disk+InternalHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 103FA3761F713332004C600F /* Disk+InternalHelpers.swift */; }; 11 | 103FA3791F713340004C600F /* Disk+VolumeInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 103FA3781F713340004C600F /* Disk+VolumeInformation.swift */; }; 12 | 104611781F27B27E00BBDB3A /* Disk+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 104611771F27B27E00BBDB3A /* Disk+Errors.swift */; }; 13 | 109256561F24576700B3C32A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 109256551F24576700B3C32A /* Images.xcassets */; }; 14 | 1092565A1F247F7700B3C32A /* Disk+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109256591F247F7700B3C32A /* Disk+Helpers.swift */; }; 15 | 10A951BE1F4AC0E200A83A78 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A951BC1F4ABE2900A83A78 /* UIImage+Extension.swift */; }; 16 | 10F3C6EB1F23D984006D42EF /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10F3C6E11F23D984006D42EF /* Disk.framework */; }; 17 | 10F3C6F01F23D984006D42EF /* DiskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C6EF1F23D984006D42EF /* DiskTests.swift */; }; 18 | 10F3C6F21F23D984006D42EF /* Disk.h in Headers */ = {isa = PBXBuildFile; fileRef = 10F3C6E41F23D984006D42EF /* Disk.h */; settings = {ATTRIBUTES = (Public, ); }; }; 19 | 10F3C6FC1F23D994006D42EF /* Disk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C6FB1F23D994006D42EF /* Disk.swift */; }; 20 | 10F3C6FE1F23D9A4006D42EF /* Disk+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C6FD1F23D9A4006D42EF /* Disk+Codable.swift */; }; 21 | 10F3C7001F23D9B1006D42EF /* Disk+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C6FF1F23D9B1006D42EF /* Disk+UIImage.swift */; }; 22 | 10F3C7021F23D9C1006D42EF /* Disk+[UIImage].swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C7011F23D9C1006D42EF /* Disk+[UIImage].swift */; }; 23 | 10F3C7041F23D9C9006D42EF /* Disk+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C7031F23D9C9006D42EF /* Disk+Data.swift */; }; 24 | 10F3C7061F23D9E0006D42EF /* Disk+[Data].swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C7051F23D9E0006D42EF /* Disk+[Data].swift */; }; 25 | 10F3C7081F23DAF2006D42EF /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3C7071F23DAF2006D42EF /* Message.swift */; }; 26 | 7948F3CA2B8DD848006F61A7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7948F3C92B8DD848006F61A7 /* PrivacyInfo.xcprivacy */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | 10F3C6EC1F23D984006D42EF /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 10F3C6D81F23D984006D42EF /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 10F3C6E01F23D984006D42EF; 35 | remoteInfo = Disk; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 103FA3761F713332004C600F /* Disk+InternalHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+InternalHelpers.swift"; sourceTree = ""; }; 41 | 103FA3781F713340004C600F /* Disk+VolumeInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+VolumeInformation.swift"; sourceTree = ""; }; 42 | 104611771F27B27E00BBDB3A /* Disk+Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+Errors.swift"; sourceTree = ""; }; 43 | 109256551F24576700B3C32A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 44 | 109256591F247F7700B3C32A /* Disk+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+Helpers.swift"; sourceTree = ""; }; 45 | 10A951BC1F4ABE2900A83A78 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; 46 | 10F3C6E11F23D984006D42EF /* Disk.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Disk.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 10F3C6E41F23D984006D42EF /* Disk.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Disk.h; sourceTree = ""; }; 48 | 10F3C6E51F23D984006D42EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | 10F3C6EA1F23D984006D42EF /* DiskTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DiskTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 10F3C6EF1F23D984006D42EF /* DiskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskTests.swift; sourceTree = ""; }; 51 | 10F3C6F11F23D984006D42EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | 10F3C6FB1F23D994006D42EF /* Disk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disk.swift; sourceTree = ""; }; 53 | 10F3C6FD1F23D9A4006D42EF /* Disk+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+Codable.swift"; sourceTree = ""; }; 54 | 10F3C6FF1F23D9B1006D42EF /* Disk+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+UIImage.swift"; sourceTree = ""; }; 55 | 10F3C7011F23D9C1006D42EF /* Disk+[UIImage].swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+[UIImage].swift"; sourceTree = ""; }; 56 | 10F3C7031F23D9C9006D42EF /* Disk+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+Data.swift"; sourceTree = ""; }; 57 | 10F3C7051F23D9E0006D42EF /* Disk+[Data].swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Disk+[Data].swift"; sourceTree = ""; }; 58 | 10F3C7071F23DAF2006D42EF /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 59 | 7948F3C92B8DD848006F61A7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 10F3C6DD1F23D984006D42EF /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | 10F3C6E71F23D984006D42EF /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | 10F3C6EB1F23D984006D42EF /* Disk.framework in Frameworks */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | /* End PBXFrameworksBuildPhase section */ 79 | 80 | /* Begin PBXGroup section */ 81 | 10F3C6D71F23D984006D42EF = { 82 | isa = PBXGroup; 83 | children = ( 84 | 10F3C6E31F23D984006D42EF /* Sources */, 85 | 10F3C6EE1F23D984006D42EF /* Tests */, 86 | 10F3C6E21F23D984006D42EF /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | 10F3C6E21F23D984006D42EF /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 10F3C6E11F23D984006D42EF /* Disk.framework */, 94 | 10F3C6EA1F23D984006D42EF /* DiskTests.xctest */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | 10F3C6E31F23D984006D42EF /* Sources */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 10F3C6E41F23D984006D42EF /* Disk.h */, 103 | 10F3C6E51F23D984006D42EF /* Info.plist */, 104 | 10F3C6FB1F23D994006D42EF /* Disk.swift */, 105 | 103FA3761F713332004C600F /* Disk+InternalHelpers.swift */, 106 | 103FA3781F713340004C600F /* Disk+VolumeInformation.swift */, 107 | 104611771F27B27E00BBDB3A /* Disk+Errors.swift */, 108 | 109256591F247F7700B3C32A /* Disk+Helpers.swift */, 109 | 10F3C6FD1F23D9A4006D42EF /* Disk+Codable.swift */, 110 | 10F3C6FF1F23D9B1006D42EF /* Disk+UIImage.swift */, 111 | 10F3C7011F23D9C1006D42EF /* Disk+[UIImage].swift */, 112 | 10F3C7031F23D9C9006D42EF /* Disk+Data.swift */, 113 | 10F3C7051F23D9E0006D42EF /* Disk+[Data].swift */, 114 | 7948F3C92B8DD848006F61A7 /* PrivacyInfo.xcprivacy */, 115 | ); 116 | path = Sources; 117 | sourceTree = ""; 118 | }; 119 | 10F3C6EE1F23D984006D42EF /* Tests */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 10F3C6EF1F23D984006D42EF /* DiskTests.swift */, 123 | 10A951BC1F4ABE2900A83A78 /* UIImage+Extension.swift */, 124 | 10F3C7071F23DAF2006D42EF /* Message.swift */, 125 | 10F3C6F11F23D984006D42EF /* Info.plist */, 126 | 109256551F24576700B3C32A /* Images.xcassets */, 127 | ); 128 | path = Tests; 129 | sourceTree = ""; 130 | }; 131 | /* End PBXGroup section */ 132 | 133 | /* Begin PBXHeadersBuildPhase section */ 134 | 10F3C6DE1F23D984006D42EF /* Headers */ = { 135 | isa = PBXHeadersBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 10F3C6F21F23D984006D42EF /* Disk.h in Headers */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXHeadersBuildPhase section */ 143 | 144 | /* Begin PBXNativeTarget section */ 145 | 10F3C6E01F23D984006D42EF /* Disk */ = { 146 | isa = PBXNativeTarget; 147 | buildConfigurationList = 10F3C6F51F23D984006D42EF /* Build configuration list for PBXNativeTarget "Disk" */; 148 | buildPhases = ( 149 | 10F3C6DC1F23D984006D42EF /* Sources */, 150 | 10F3C6DD1F23D984006D42EF /* Frameworks */, 151 | 10F3C6DE1F23D984006D42EF /* Headers */, 152 | 10F3C6DF1F23D984006D42EF /* Resources */, 153 | ); 154 | buildRules = ( 155 | ); 156 | dependencies = ( 157 | ); 158 | name = Disk; 159 | productName = Disk; 160 | productReference = 10F3C6E11F23D984006D42EF /* Disk.framework */; 161 | productType = "com.apple.product-type.framework"; 162 | }; 163 | 10F3C6E91F23D984006D42EF /* DiskTests */ = { 164 | isa = PBXNativeTarget; 165 | buildConfigurationList = 10F3C6F81F23D984006D42EF /* Build configuration list for PBXNativeTarget "DiskTests" */; 166 | buildPhases = ( 167 | 10F3C6E61F23D984006D42EF /* Sources */, 168 | 10F3C6E71F23D984006D42EF /* Frameworks */, 169 | 10F3C6E81F23D984006D42EF /* Resources */, 170 | ); 171 | buildRules = ( 172 | ); 173 | dependencies = ( 174 | 10F3C6ED1F23D984006D42EF /* PBXTargetDependency */, 175 | ); 176 | name = DiskTests; 177 | productName = DiskTests; 178 | productReference = 10F3C6EA1F23D984006D42EF /* DiskTests.xctest */; 179 | productType = "com.apple.product-type.bundle.unit-test"; 180 | }; 181 | /* End PBXNativeTarget section */ 182 | 183 | /* Begin PBXProject section */ 184 | 10F3C6D81F23D984006D42EF /* Project object */ = { 185 | isa = PBXProject; 186 | attributes = { 187 | LastSwiftUpdateCheck = 0900; 188 | LastUpgradeCheck = 1020; 189 | ORGANIZATIONNAME = "Saoud Rizwan"; 190 | TargetAttributes = { 191 | 10F3C6E01F23D984006D42EF = { 192 | CreatedOnToolsVersion = 9.0; 193 | LastSwiftMigration = 0900; 194 | }; 195 | 10F3C6E91F23D984006D42EF = { 196 | CreatedOnToolsVersion = 9.0; 197 | ProvisioningStyle = Manual; 198 | }; 199 | }; 200 | }; 201 | buildConfigurationList = 10F3C6DB1F23D984006D42EF /* Build configuration list for PBXProject "Disk" */; 202 | compatibilityVersion = "Xcode 8.0"; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | ); 209 | mainGroup = 10F3C6D71F23D984006D42EF; 210 | productRefGroup = 10F3C6E21F23D984006D42EF /* Products */; 211 | projectDirPath = ""; 212 | projectRoot = ""; 213 | targets = ( 214 | 10F3C6E01F23D984006D42EF /* Disk */, 215 | 10F3C6E91F23D984006D42EF /* DiskTests */, 216 | ); 217 | }; 218 | /* End PBXProject section */ 219 | 220 | /* Begin PBXResourcesBuildPhase section */ 221 | 10F3C6DF1F23D984006D42EF /* Resources */ = { 222 | isa = PBXResourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 7948F3CA2B8DD848006F61A7 /* PrivacyInfo.xcprivacy in Resources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | 10F3C6E81F23D984006D42EF /* Resources */ = { 230 | isa = PBXResourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | 109256561F24576700B3C32A /* Images.xcassets in Resources */, 234 | ); 235 | runOnlyForDeploymentPostprocessing = 0; 236 | }; 237 | /* End PBXResourcesBuildPhase section */ 238 | 239 | /* Begin PBXSourcesBuildPhase section */ 240 | 10F3C6DC1F23D984006D42EF /* Sources */ = { 241 | isa = PBXSourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | 10F3C6FC1F23D994006D42EF /* Disk.swift in Sources */, 245 | 103FA3771F713332004C600F /* Disk+InternalHelpers.swift in Sources */, 246 | 10F3C6FE1F23D9A4006D42EF /* Disk+Codable.swift in Sources */, 247 | 1092565A1F247F7700B3C32A /* Disk+Helpers.swift in Sources */, 248 | 10F3C7061F23D9E0006D42EF /* Disk+[Data].swift in Sources */, 249 | 10F3C7021F23D9C1006D42EF /* Disk+[UIImage].swift in Sources */, 250 | 10F3C7041F23D9C9006D42EF /* Disk+Data.swift in Sources */, 251 | 103FA3791F713340004C600F /* Disk+VolumeInformation.swift in Sources */, 252 | 10F3C7001F23D9B1006D42EF /* Disk+UIImage.swift in Sources */, 253 | 104611781F27B27E00BBDB3A /* Disk+Errors.swift in Sources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | 10F3C6E61F23D984006D42EF /* Sources */ = { 258 | isa = PBXSourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | 10A951BE1F4AC0E200A83A78 /* UIImage+Extension.swift in Sources */, 262 | 10F3C6F01F23D984006D42EF /* DiskTests.swift in Sources */, 263 | 10F3C7081F23DAF2006D42EF /* Message.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | /* End PBXSourcesBuildPhase section */ 268 | 269 | /* Begin PBXTargetDependency section */ 270 | 10F3C6ED1F23D984006D42EF /* PBXTargetDependency */ = { 271 | isa = PBXTargetDependency; 272 | target = 10F3C6E01F23D984006D42EF /* Disk */; 273 | targetProxy = 10F3C6EC1F23D984006D42EF /* PBXContainerItemProxy */; 274 | }; 275 | /* End PBXTargetDependency section */ 276 | 277 | /* Begin XCBuildConfiguration section */ 278 | 10F3C6F31F23D984006D42EF /* Debug */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ALWAYS_SEARCH_USER_PATHS = NO; 282 | CLANG_ANALYZER_NONNULL = YES; 283 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 284 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 285 | CLANG_CXX_LIBRARY = "libc++"; 286 | CLANG_ENABLE_MODULES = YES; 287 | CLANG_ENABLE_OBJC_ARC = YES; 288 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 289 | CLANG_WARN_BOOL_CONVERSION = YES; 290 | CLANG_WARN_COMMA = YES; 291 | CLANG_WARN_CONSTANT_CONVERSION = YES; 292 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 295 | CLANG_WARN_EMPTY_BODY = YES; 296 | CLANG_WARN_ENUM_CONVERSION = YES; 297 | CLANG_WARN_INFINITE_RECURSION = YES; 298 | CLANG_WARN_INT_CONVERSION = YES; 299 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 300 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 301 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 302 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 303 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 304 | CLANG_WARN_STRICT_PROTOTYPES = YES; 305 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 306 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 307 | CLANG_WARN_UNREACHABLE_CODE = YES; 308 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 309 | CODE_SIGN_IDENTITY = "iPhone Developer"; 310 | COPY_PHASE_STRIP = NO; 311 | CURRENT_PROJECT_VERSION = 1; 312 | DEBUG_INFORMATION_FORMAT = dwarf; 313 | ENABLE_STRICT_OBJC_MSGSEND = YES; 314 | ENABLE_TESTABILITY = YES; 315 | GCC_C_LANGUAGE_STANDARD = gnu11; 316 | GCC_DYNAMIC_NO_PIC = NO; 317 | GCC_NO_COMMON_BLOCKS = YES; 318 | GCC_OPTIMIZATION_LEVEL = 0; 319 | GCC_PREPROCESSOR_DEFINITIONS = ( 320 | "DEBUG=1", 321 | "$(inherited)", 322 | ); 323 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 324 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 325 | GCC_WARN_UNDECLARED_SELECTOR = YES; 326 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 327 | GCC_WARN_UNUSED_FUNCTION = YES; 328 | GCC_WARN_UNUSED_VARIABLE = YES; 329 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 330 | MTL_ENABLE_DEBUG_INFO = YES; 331 | ONLY_ACTIVE_ARCH = YES; 332 | SDKROOT = iphoneos; 333 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 334 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 335 | SWIFT_VERSION = 5.0; 336 | VERSIONING_SYSTEM = "apple-generic"; 337 | VERSION_INFO_PREFIX = ""; 338 | }; 339 | name = Debug; 340 | }; 341 | 10F3C6F41F23D984006D42EF /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ALWAYS_SEARCH_USER_PATHS = NO; 345 | CLANG_ANALYZER_NONNULL = YES; 346 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 348 | CLANG_CXX_LIBRARY = "libc++"; 349 | CLANG_ENABLE_MODULES = YES; 350 | CLANG_ENABLE_OBJC_ARC = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 367 | CLANG_WARN_STRICT_PROTOTYPES = YES; 368 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 369 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | CODE_SIGN_IDENTITY = "iPhone Developer"; 373 | COPY_PHASE_STRIP = NO; 374 | CURRENT_PROJECT_VERSION = 1; 375 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 376 | ENABLE_NS_ASSERTIONS = NO; 377 | ENABLE_STRICT_OBJC_MSGSEND = YES; 378 | GCC_C_LANGUAGE_STANDARD = gnu11; 379 | GCC_NO_COMMON_BLOCKS = YES; 380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 381 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 382 | GCC_WARN_UNDECLARED_SELECTOR = YES; 383 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 384 | GCC_WARN_UNUSED_FUNCTION = YES; 385 | GCC_WARN_UNUSED_VARIABLE = YES; 386 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 387 | MTL_ENABLE_DEBUG_INFO = NO; 388 | SDKROOT = iphoneos; 389 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 390 | SWIFT_VERSION = 5.0; 391 | VALIDATE_PRODUCT = YES; 392 | VERSIONING_SYSTEM = "apple-generic"; 393 | VERSION_INFO_PREFIX = ""; 394 | }; 395 | name = Release; 396 | }; 397 | 10F3C6F61F23D984006D42EF /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | APPLICATION_EXTENSION_API_ONLY = YES; 401 | CLANG_ENABLE_MODULES = YES; 402 | CODE_SIGN_IDENTITY = ""; 403 | DEFINES_MODULE = YES; 404 | DEVELOPMENT_TEAM = ""; 405 | DYLIB_COMPATIBILITY_VERSION = 1; 406 | DYLIB_CURRENT_VERSION = 1; 407 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 408 | INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; 409 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 410 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 411 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 412 | PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.Disk; 413 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 414 | SKIP_INSTALL = YES; 415 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 416 | SWIFT_VERSION = 5.0; 417 | TARGETED_DEVICE_FAMILY = "1,2"; 418 | }; 419 | name = Debug; 420 | }; 421 | 10F3C6F71F23D984006D42EF /* Release */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | APPLICATION_EXTENSION_API_ONLY = YES; 425 | CLANG_ENABLE_MODULES = YES; 426 | CODE_SIGN_IDENTITY = ""; 427 | DEFINES_MODULE = YES; 428 | DEVELOPMENT_TEAM = ""; 429 | DYLIB_COMPATIBILITY_VERSION = 1; 430 | DYLIB_CURRENT_VERSION = 1; 431 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 432 | INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; 433 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 434 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 435 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 436 | PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.Disk; 437 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 438 | SKIP_INSTALL = YES; 439 | SWIFT_VERSION = 5.0; 440 | TARGETED_DEVICE_FAMILY = "1,2"; 441 | }; 442 | name = Release; 443 | }; 444 | 10F3C6F91F23D984006D42EF /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 448 | CODE_SIGN_STYLE = Manual; 449 | DEVELOPMENT_TEAM = ""; 450 | INFOPLIST_FILE = Tests/Info.plist; 451 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 452 | PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.DiskTests; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | PROVISIONING_PROFILE_SPECIFIER = ""; 455 | SWIFT_VERSION = 5.0; 456 | TARGETED_DEVICE_FAMILY = "1,2"; 457 | }; 458 | name = Debug; 459 | }; 460 | 10F3C6FA1F23D984006D42EF /* Release */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 464 | CODE_SIGN_STYLE = Manual; 465 | DEVELOPMENT_TEAM = ""; 466 | INFOPLIST_FILE = Tests/Info.plist; 467 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 468 | PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.DiskTests; 469 | PRODUCT_NAME = "$(TARGET_NAME)"; 470 | PROVISIONING_PROFILE_SPECIFIER = ""; 471 | SWIFT_VERSION = 5.0; 472 | TARGETED_DEVICE_FAMILY = "1,2"; 473 | }; 474 | name = Release; 475 | }; 476 | /* End XCBuildConfiguration section */ 477 | 478 | /* Begin XCConfigurationList section */ 479 | 10F3C6DB1F23D984006D42EF /* Build configuration list for PBXProject "Disk" */ = { 480 | isa = XCConfigurationList; 481 | buildConfigurations = ( 482 | 10F3C6F31F23D984006D42EF /* Debug */, 483 | 10F3C6F41F23D984006D42EF /* Release */, 484 | ); 485 | defaultConfigurationIsVisible = 0; 486 | defaultConfigurationName = Release; 487 | }; 488 | 10F3C6F51F23D984006D42EF /* Build configuration list for PBXNativeTarget "Disk" */ = { 489 | isa = XCConfigurationList; 490 | buildConfigurations = ( 491 | 10F3C6F61F23D984006D42EF /* Debug */, 492 | 10F3C6F71F23D984006D42EF /* Release */, 493 | ); 494 | defaultConfigurationIsVisible = 0; 495 | defaultConfigurationName = Release; 496 | }; 497 | 10F3C6F81F23D984006D42EF /* Build configuration list for PBXNativeTarget "DiskTests" */ = { 498 | isa = XCConfigurationList; 499 | buildConfigurations = ( 500 | 10F3C6F91F23D984006D42EF /* Debug */, 501 | 10F3C6FA1F23D984006D42EF /* Release */, 502 | ); 503 | defaultConfigurationIsVisible = 0; 504 | defaultConfigurationName = Release; 505 | }; 506 | /* End XCConfigurationList section */ 507 | }; 508 | rootObject = 10F3C6D81F23D984006D42EF /* Project object */; 509 | } 510 | -------------------------------------------------------------------------------- /Disk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Disk.xcodeproj/xcshareddata/xcschemes/Disk.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Disk.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /DiskExample/DiskExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 109254311F25C65100A93A13 /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 109254301F25C65100A93A13 /* Disk.framework */; }; 11 | 109254321F25C65100A93A13 /* Disk.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 109254301F25C65100A93A13 /* Disk.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | 109256681F248E8800B3C32A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109256671F248E8800B3C32A /* AppDelegate.swift */; }; 13 | 1092566A1F248E8800B3C32A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109256691F248E8800B3C32A /* ViewController.swift */; }; 14 | 1092566D1F248E8800B3C32A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1092566B1F248E8800B3C32A /* Main.storyboard */; }; 15 | 1092566F1F248E8800B3C32A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1092566E1F248E8800B3C32A /* Assets.xcassets */; }; 16 | 109256721F248E8800B3C32A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 109256701F248E8800B3C32A /* LaunchScreen.storyboard */; }; 17 | 1092567E1F24922500B3C32A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1092567D1F24922500B3C32A /* Post.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 109254331F25C65100A93A13 /* Embed Frameworks */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = ""; 25 | dstSubfolderSpec = 10; 26 | files = ( 27 | 109254321F25C65100A93A13 /* Disk.framework in Embed Frameworks */, 28 | ); 29 | name = "Embed Frameworks"; 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXCopyFilesBuildPhase section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 109254301F25C65100A93A13 /* Disk.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Disk.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 109256641F248E8800B3C32A /* DiskExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DiskExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 109256671F248E8800B3C32A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 109256691F248E8800B3C32A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 39 | 1092566C1F248E8800B3C32A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 40 | 1092566E1F248E8800B3C32A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 109256711F248E8800B3C32A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42 | 109256731F248E8800B3C32A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 1092567D1F24922500B3C32A /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | 109256611F248E8800B3C32A /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | 109254311F25C65100A93A13 /* Disk.framework in Frameworks */, 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 1092565B1F248E8800B3C32A = { 59 | isa = PBXGroup; 60 | children = ( 61 | 109256661F248E8800B3C32A /* DiskExample */, 62 | 109256651F248E8800B3C32A /* Products */, 63 | 43A00271DAED0D8D01836231 /* Frameworks */, 64 | ); 65 | sourceTree = ""; 66 | }; 67 | 109256651F248E8800B3C32A /* Products */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 109256641F248E8800B3C32A /* DiskExample.app */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | 109256661F248E8800B3C32A /* DiskExample */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 109256671F248E8800B3C32A /* AppDelegate.swift */, 79 | 1092567D1F24922500B3C32A /* Post.swift */, 80 | 109256691F248E8800B3C32A /* ViewController.swift */, 81 | 1092566B1F248E8800B3C32A /* Main.storyboard */, 82 | 1092566E1F248E8800B3C32A /* Assets.xcassets */, 83 | 109256701F248E8800B3C32A /* LaunchScreen.storyboard */, 84 | 109256731F248E8800B3C32A /* Info.plist */, 85 | ); 86 | path = DiskExample; 87 | sourceTree = ""; 88 | }; 89 | 43A00271DAED0D8D01836231 /* Frameworks */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 109254301F25C65100A93A13 /* Disk.framework */, 93 | ); 94 | name = Frameworks; 95 | sourceTree = ""; 96 | }; 97 | /* End PBXGroup section */ 98 | 99 | /* Begin PBXNativeTarget section */ 100 | 109256631F248E8800B3C32A /* DiskExample */ = { 101 | isa = PBXNativeTarget; 102 | buildConfigurationList = 109256761F248E8800B3C32A /* Build configuration list for PBXNativeTarget "DiskExample" */; 103 | buildPhases = ( 104 | 109256601F248E8800B3C32A /* Sources */, 105 | 109256611F248E8800B3C32A /* Frameworks */, 106 | 109256621F248E8800B3C32A /* Resources */, 107 | 109254331F25C65100A93A13 /* Embed Frameworks */, 108 | ); 109 | buildRules = ( 110 | ); 111 | dependencies = ( 112 | ); 113 | name = DiskExample; 114 | productName = DiskExample; 115 | productReference = 109256641F248E8800B3C32A /* DiskExample.app */; 116 | productType = "com.apple.product-type.application"; 117 | }; 118 | /* End PBXNativeTarget section */ 119 | 120 | /* Begin PBXProject section */ 121 | 1092565C1F248E8800B3C32A /* Project object */ = { 122 | isa = PBXProject; 123 | attributes = { 124 | LastSwiftUpdateCheck = 0900; 125 | LastUpgradeCheck = 1020; 126 | ORGANIZATIONNAME = "Saoud Rizwan"; 127 | TargetAttributes = { 128 | 109256631F248E8800B3C32A = { 129 | CreatedOnToolsVersion = 9.0; 130 | LastSwiftMigration = 1020; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 1092565F1F248E8800B3C32A /* Build configuration list for PBXProject "DiskExample" */; 135 | compatibilityVersion = "Xcode 8.0"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = 1092565B1F248E8800B3C32A; 143 | productRefGroup = 109256651F248E8800B3C32A /* Products */; 144 | projectDirPath = ""; 145 | projectRoot = ""; 146 | targets = ( 147 | 109256631F248E8800B3C32A /* DiskExample */, 148 | ); 149 | }; 150 | /* End PBXProject section */ 151 | 152 | /* Begin PBXResourcesBuildPhase section */ 153 | 109256621F248E8800B3C32A /* Resources */ = { 154 | isa = PBXResourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 109256721F248E8800B3C32A /* LaunchScreen.storyboard in Resources */, 158 | 1092566F1F248E8800B3C32A /* Assets.xcassets in Resources */, 159 | 1092566D1F248E8800B3C32A /* Main.storyboard in Resources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXResourcesBuildPhase section */ 164 | 165 | /* Begin PBXSourcesBuildPhase section */ 166 | 109256601F248E8800B3C32A /* Sources */ = { 167 | isa = PBXSourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | 1092567E1F24922500B3C32A /* Post.swift in Sources */, 171 | 1092566A1F248E8800B3C32A /* ViewController.swift in Sources */, 172 | 109256681F248E8800B3C32A /* AppDelegate.swift in Sources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXSourcesBuildPhase section */ 177 | 178 | /* Begin PBXVariantGroup section */ 179 | 1092566B1F248E8800B3C32A /* Main.storyboard */ = { 180 | isa = PBXVariantGroup; 181 | children = ( 182 | 1092566C1F248E8800B3C32A /* Base */, 183 | ); 184 | name = Main.storyboard; 185 | sourceTree = ""; 186 | }; 187 | 109256701F248E8800B3C32A /* LaunchScreen.storyboard */ = { 188 | isa = PBXVariantGroup; 189 | children = ( 190 | 109256711F248E8800B3C32A /* Base */, 191 | ); 192 | name = LaunchScreen.storyboard; 193 | sourceTree = ""; 194 | }; 195 | /* End PBXVariantGroup section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | 109256741F248E8800B3C32A /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_ANALYZER_NONNULL = YES; 203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 205 | CLANG_CXX_LIBRARY = "libc++"; 206 | CLANG_ENABLE_MODULES = YES; 207 | CLANG_ENABLE_OBJC_ARC = YES; 208 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 209 | CLANG_WARN_BOOL_CONVERSION = YES; 210 | CLANG_WARN_COMMA = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 213 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 214 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 215 | CLANG_WARN_EMPTY_BODY = YES; 216 | CLANG_WARN_ENUM_CONVERSION = YES; 217 | CLANG_WARN_INFINITE_RECURSION = YES; 218 | CLANG_WARN_INT_CONVERSION = YES; 219 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 220 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 221 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 222 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | CODE_SIGN_IDENTITY = "iPhone Developer"; 230 | COPY_PHASE_STRIP = NO; 231 | DEBUG_INFORMATION_FORMAT = dwarf; 232 | ENABLE_STRICT_OBJC_MSGSEND = YES; 233 | ENABLE_TESTABILITY = YES; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 249 | MTL_ENABLE_DEBUG_INFO = YES; 250 | ONLY_ACTIVE_ARCH = YES; 251 | SDKROOT = iphoneos; 252 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 253 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 254 | }; 255 | name = Debug; 256 | }; 257 | 109256751F248E8800B3C32A /* Release */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | CLANG_ANALYZER_NONNULL = YES; 262 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 263 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 264 | CLANG_CXX_LIBRARY = "libc++"; 265 | CLANG_ENABLE_MODULES = YES; 266 | CLANG_ENABLE_OBJC_ARC = YES; 267 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 268 | CLANG_WARN_BOOL_CONVERSION = YES; 269 | CLANG_WARN_COMMA = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 272 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 273 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 274 | CLANG_WARN_EMPTY_BODY = YES; 275 | CLANG_WARN_ENUM_CONVERSION = YES; 276 | CLANG_WARN_INFINITE_RECURSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 279 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 280 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 282 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 283 | CLANG_WARN_STRICT_PROTOTYPES = YES; 284 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 285 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 286 | CLANG_WARN_UNREACHABLE_CODE = YES; 287 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 288 | CODE_SIGN_IDENTITY = "iPhone Developer"; 289 | COPY_PHASE_STRIP = NO; 290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 291 | ENABLE_NS_ASSERTIONS = NO; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | GCC_C_LANGUAGE_STANDARD = gnu11; 294 | GCC_NO_COMMON_BLOCKS = YES; 295 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 296 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 297 | GCC_WARN_UNDECLARED_SELECTOR = YES; 298 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 299 | GCC_WARN_UNUSED_FUNCTION = YES; 300 | GCC_WARN_UNUSED_VARIABLE = YES; 301 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 302 | MTL_ENABLE_DEBUG_INFO = NO; 303 | SDKROOT = iphoneos; 304 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 305 | VALIDATE_PRODUCT = YES; 306 | }; 307 | name = Release; 308 | }; 309 | 109256771F248E8800B3C32A /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 313 | DEVELOPMENT_TEAM = ""; 314 | INFOPLIST_FILE = DiskExample/Info.plist; 315 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 316 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 317 | PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.DiskExample; 318 | PRODUCT_NAME = "$(TARGET_NAME)"; 319 | SWIFT_VERSION = 5.0; 320 | TARGETED_DEVICE_FAMILY = "1,2"; 321 | }; 322 | name = Debug; 323 | }; 324 | 109256781F248E8800B3C32A /* Release */ = { 325 | isa = XCBuildConfiguration; 326 | buildSettings = { 327 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 328 | DEVELOPMENT_TEAM = ""; 329 | INFOPLIST_FILE = DiskExample/Info.plist; 330 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 331 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 332 | PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.DiskExample; 333 | PRODUCT_NAME = "$(TARGET_NAME)"; 334 | SWIFT_VERSION = 5.0; 335 | TARGETED_DEVICE_FAMILY = "1,2"; 336 | }; 337 | name = Release; 338 | }; 339 | /* End XCBuildConfiguration section */ 340 | 341 | /* Begin XCConfigurationList section */ 342 | 1092565F1F248E8800B3C32A /* Build configuration list for PBXProject "DiskExample" */ = { 343 | isa = XCConfigurationList; 344 | buildConfigurations = ( 345 | 109256741F248E8800B3C32A /* Debug */, 346 | 109256751F248E8800B3C32A /* Release */, 347 | ); 348 | defaultConfigurationIsVisible = 0; 349 | defaultConfigurationName = Release; 350 | }; 351 | 109256761F248E8800B3C32A /* Build configuration list for PBXNativeTarget "DiskExample" */ = { 352 | isa = XCConfigurationList; 353 | buildConfigurations = ( 354 | 109256771F248E8800B3C32A /* Debug */, 355 | 109256781F248E8800B3C32A /* Release */, 356 | ); 357 | defaultConfigurationIsVisible = 0; 358 | defaultConfigurationName = Release; 359 | }; 360 | /* End XCConfigurationList section */ 361 | }; 362 | rootObject = 1092565C1F248E8800B3C32A /* Project object */; 363 | } 364 | -------------------------------------------------------------------------------- /DiskExample/DiskExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DiskExample/DiskExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DiskExample 4 | // 5 | // Created by Saoud Rizwan on 7/23/17. 6 | // Copyright © 2017 Saoud Rizwan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /DiskExample/DiskExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /DiskExample/DiskExample/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 | -------------------------------------------------------------------------------- /DiskExample/DiskExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 30 | 39 | 49 | 58 | 68 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 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 | -------------------------------------------------------------------------------- /DiskExample/DiskExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | NSAppTransportSecurity 38 | 39 | NSAllowsArbitraryLoads 40 | 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /DiskExample/DiskExample/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // DiskExample 4 | // 5 | // Created by Saoud Rizwan on 7/23/17. 6 | // Copyright © 2017 Saoud Rizwan. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Post: Codable { 12 | let userId: Int 13 | let id: Int 14 | let title: String 15 | let body: String 16 | } 17 | -------------------------------------------------------------------------------- /DiskExample/DiskExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DiskExample 4 | // 5 | // Created by Saoud Rizwan on 7/23/17. 6 | // Copyright © 2017 Saoud Rizwan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Disk 11 | 12 | class ViewController: UIViewController { 13 | 14 | // MARK: Properties 15 | 16 | var posts = [Post]() 17 | 18 | // MARK: IBOutlets 19 | 20 | @IBOutlet weak var resultsTextView: UITextView! 21 | 22 | // MARK: IBActions 23 | 24 | @IBAction func getTapped(_ sender: Any) { 25 | // Be sure to check out the comments in the networking function below 26 | UIApplication.shared.isNetworkActivityIndicatorVisible = true 27 | getPostsFromWeb { (posts) in 28 | UIApplication.shared.isNetworkActivityIndicatorVisible = false 29 | print("Posts retrieved from network request successfully!") 30 | self.posts = posts 31 | } 32 | } 33 | 34 | @IBAction func saveTapped(_ sender: Any) { 35 | // Disk is thorough when it comes to error handling, so make sure you understand why an error occurs when it does. 36 | do { 37 | try Disk.save(self.posts, to: .documents, as: "posts.json") 38 | } catch let error as NSError { 39 | fatalError(""" 40 | Domain: \(error.domain) 41 | Code: \(error.code) 42 | Description: \(error.localizedDescription) 43 | Failure Reason: \(error.localizedFailureReason ?? "") 44 | Suggestions: \(error.localizedRecoverySuggestion ?? "") 45 | """) 46 | } 47 | // Notice how we use a do, catch, try block when using Disk, this is because almost all of Disk's methods 48 | // are throwing functions, meaning they will throw an error if something goes wrong. In almost all cases, these 49 | // errors come with a lot of information like a description, failure reason, and recover suggestions. 50 | 51 | // You could alternatively use try! or try? instead of do, catch, try blocks 52 | // try? Disk.save(self.posts, to: .documents, as: "posts.json") // returns a discardable result of nil 53 | // try! Disk.save(self.posts, to: .documents, as: "posts.json") // will crash the app during runtime if this fails 54 | 55 | // You can also save files in folder hierarchies, for example: 56 | // try? Disk.save(self.posts, to: .caches, as: "Posts/MyCoolPosts/1.json") 57 | // This will automatically create the Posts and MyCoolPosts folders 58 | 59 | // If you want to save new data to a file location, you can treat the file as an array and simply append to it as well. 60 | let newPost = Post(userId: 0, id: self.posts.count + 1, title: "Appended Post", body: "...") 61 | try? Disk.append(newPost, to: "posts.json", in: .documents) 62 | 63 | print("Saved posts to disk!") 64 | } 65 | 66 | @IBAction func retrieveTapped(_ sender: Any) { 67 | // We'll keep things simple here by using try?, but it's good practice to handle Disk with do, catch, try blocks 68 | // so you can make sure everything is going according to plan. 69 | if let retrievedPosts = try? Disk.retrieve("posts.json", from: .documents, as: [Post].self) { 70 | // If you Option+Click 'retrievedPosts' above, you'll notice that its type is [Post] 71 | // Pretty neat, huh? 72 | 73 | var result: String = "" 74 | for post in retrievedPosts { 75 | result.append("\(post.id): \(post.title)\n\(post.body)\n\n") 76 | } 77 | self.resultsTextView.text = result 78 | 79 | print("Retrieved posts from disk!") 80 | } 81 | } 82 | 83 | // MARK: Networking 84 | 85 | func getPostsFromWeb(completion: (([Post]) -> Void)?) { 86 | var urlComponents = URLComponents() 87 | urlComponents.scheme = "https" 88 | urlComponents.host = "jsonplaceholder.typicode.com" 89 | urlComponents.path = "/posts" 90 | let userIdItem = URLQueryItem(name: "userId", value: "1") 91 | urlComponents.queryItems = [userIdItem] 92 | guard let url = urlComponents.url else { fatalError("Could not create URL from components") } 93 | var request = URLRequest(url: url) 94 | request.httpMethod = "GET" 95 | let config = URLSessionConfiguration.default 96 | let session = URLSession(configuration: config) 97 | let task = session.dataTask(with: request) { (data, response, error) in 98 | DispatchQueue.main.async { 99 | guard error == nil else { fatalError(error!.localizedDescription) } 100 | guard let data = data else { fatalError("No data retrieved") } 101 | 102 | // We could directly save this data to disk... 103 | // try? Disk.save(data, to: .caches, as: "posts.json") 104 | 105 | // ... and retrieve it later as [Post]... 106 | // let posts = try? Disk.retrieve("posts.json", from: .caches, as: [Post].self) 107 | 108 | // ... but that's not good practice! Our networking and persistence logic should be separate. 109 | // Let's return the posts in our completion handler: 110 | do { 111 | let decoder = JSONDecoder() 112 | let posts = try decoder.decode([Post].self, from: data) 113 | completion?(posts) 114 | } catch { 115 | fatalError(error.localizedDescription) 116 | } 117 | } 118 | } 119 | task.resume() 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Saoud Rizwan 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Disk", 6 | platforms: [.iOS(.v9)], 7 | products: [ 8 | .library(name: "Disk", targets: ["Disk"]) 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Disk", 13 | path: "Sources", 14 | exclude: ["DiskExample"] 15 | ), 16 | .testTarget( 17 | name: "DiskTests", 18 | dependencies: ["Disk"], 19 | path: "Tests", 20 | exclude: ["DiskExample"] 21 | ) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Disk 3 |

4 | 5 |

6 | Platform: iOS 9.0+ 7 | Language: Swift 4 8 | Carthage compatible 9 | License: MIT 10 |

11 | 12 |

13 | Installation 14 | • Usage 15 | • Debugging 16 | • A Word 17 | • Documentation 18 | • Apps Using Disk 19 | • License 20 | • Contribute 21 | • Questions? 22 |

23 | 24 | Disk is a **powerful** and **simple** file management library built with Apple's [iOS Data Storage Guidelines](https://developer.apple.com/icloud/documentation/data-storage/index.html) in mind. Disk uses the new `Codable` protocol introduced in Swift 4 to its utmost advantage and gives you the power to persist structs without ever having to worry about encoding/decoding. Disk also helps you save images and other data types to disk with as little as one line of code. 25 | 26 | ## Compatibility 27 | 28 | Disk requires **iOS 9+** and is compatible with projects using **Swift 4.0** and above. Therefore you must use at least Xcode 9 when working with Disk. 29 | 30 | ## Installation 31 | 32 | * CocoaPods: 33 | 34 | Disk supports [CocoaPods 1.7.0's new multi-Swift feature](http://blog.cocoapods.org/CocoaPods-1.7.0-beta/) for Swift 4.0, 4.2, and 5.0. Simply specify `supports_swift_versions` in your Podfile. 35 | 36 | ```ruby 37 | platform :ios, '9.0' 38 | target 'ProjectName' do 39 | use_frameworks! 40 | supports_swift_versions '< 5.0' # configure this for your project 41 | 42 | pod 'Disk', '~> 0.6.4' 43 | 44 | end 45 | ``` 46 | *(if you run into problems, `pod repo update` and try again)* 47 | 48 | * Carthage: 49 | 50 | ```ruby 51 | github "saoudrizwan/Disk" 52 | ``` 53 | 54 | * Swift Package Manager: 55 | 56 | ``` 57 | dependencies: [ 58 | .Package(url: "https://github.com/saoudrizwan/Disk.git", "0.6.4") 59 | ] 60 | ``` 61 | 62 | * Or embed the Disk framework into your project 63 | 64 | And `import Disk` in the files you'd like to use it. 65 | 66 | ## Usage 67 | 68 | Disk currently supports persistence of the following types: 69 | 70 | * `Codable` 71 | * `[Codable]` 72 | * `UIImage` 73 | * `[UIImage]` 74 | * `Data` 75 | * `[Data]` 76 | 77 | *These are generally the only types you'll ever need to persist on iOS.* 78 | 79 | Disk follows Apple's [iOS Data Storage Guidelines](https://developer.apple.com/icloud/documentation/data-storage/index.html) and therefore allows you to save files in four primary directories and shared containers: 80 | 81 | #### Documents Directory `.documents` 82 | 83 | > Only documents and other data that is **user-generated, or that cannot otherwise be recreated by your application**, should be stored in the `/Documents` directory and will be automatically backed up by iCloud. 84 | 85 | #### Caches Directory `.caches` 86 | 87 | > Data that **can be downloaded again or regenerated** should be stored in the `/Library/Caches` directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications. 88 | > 89 | > Use this directory to write any application-specific support files that you want to persist between launches of the application or during application updates. **Your application is generally responsible for adding and removing these files** (see [Helper Methods](#helper-methods)). It should also be able to re-create these files as needed because iTunes removes them during a full restoration of the device. In iOS 2.2 and later, the contents of this directory are not backed up by iTunes. 90 | > 91 | > Note that the system may delete the Caches/ directory to free up disk space, so your app must be able to re-create or download these files as needed. 92 | 93 | #### Application Support Directory `.applicationSupport` 94 | 95 | > Put app-created support files in the `/Library/Application Support` directory. In general, this directory includes files that the app uses to run but that should remain hidden from the user. This directory can also include data files, configuration files, templates and modified versions of resources loaded from the app bundle. 96 | 97 | #### Temporary Directory `.temporary` 98 | 99 | > Data that is used only temporarily should be stored in the `/tmp` directory. Although these files are not backed up to iCloud, remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device. 100 | 101 | #### Application Group Shared Container `.sharedContainer(appGroupName: String)` 102 | 103 | Multiple applications on a single device can access a shared directory, as long as these apps have the same `groupIdentifier` in the `com.apple.security.application-groups` entitlements array, as described in [Adding an App to an App Group](https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html#//apple_ref/doc/uid/TP40011195-CH4-SW19) in [Entitlement Key Reference](https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AboutEntitlements.html#//apple_ref/doc/uid/TP40011195). 104 | 105 | For more information, visit the documentation: [https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati](https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati) 106 | 107 | --- 108 | 109 | With all these requirements and best practices, it can be hard working with the iOS file system appropriately, which is why Disk was born. Disk makes following these tedious rules simple and fun. 110 | 111 | ### Using Disk is easy. 112 | 113 | Disk handles errors by `throw`ing them. See [Handling Errors Using Do-Catch](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html). 114 | 115 | ### Structs (must conform to [`Codable`](https://developer.apple.com/documentation/swift/codable)) 116 | 117 | Let's say we have a data model called `Message`... 118 | ```swift 119 | struct Message: Codable { 120 | let title: String 121 | let body: String 122 | } 123 | ``` 124 | ... and we want to persist a message to disk... 125 | ```swift 126 | let message = Message(title: "Hello", body: "How are you?") 127 | ``` 128 | ```swift 129 | try Disk.save(message, to: .caches, as: "message.json") 130 | ``` 131 | ... or maybe we want to save it in a folder... 132 | ```swift 133 | try Disk.save(message, to: .caches, as: "Folder/message.json") 134 | ``` 135 | ... we might then want to retrieve this message later... 136 | ```swift 137 | let retrievedMessage = try Disk.retrieve("Folder/message.json", from: .caches, as: Message.self) 138 | ``` 139 | If you Option + click `retrievedMessage`, then Xcode will show its type as `Message`. Pretty neat, huh? 140 | example 141 | 142 | So what happened in the background? Disk first converts `message` to JSON data and [atomically writes](https://stackoverflow.com/a/8548318/3502608) that data to a newly created file at `/Library/Caches/Folder/message.json`. Then when we retrieve the `message`, Disk automatically converts the JSON data to our `Codable` struct type. 143 | 144 | **What about arrays of structs?** 145 | 146 | Thanks to the power of `Codable`, storing and retrieving arrays of structs is just as easy as the code above. 147 | ```swift 148 | var messages = [Message]() 149 | for i in 0..<5 { 150 | messages.append(Message(title: "\(i)", body: "...")) 151 | } 152 | ``` 153 | ```swift 154 | try Disk.save(messages, to: .caches, as: "messages.json") 155 | ``` 156 | ```swift 157 | let retrievedMessages = try Disk.retrieve("messages.json", from: .caches, as: [Message].self) 158 | ``` 159 | 160 | **Appending structs** *(Thank you for the suggestion [@benpackard](https://github.com/saoudrizwan/Disk/issues/4))* 161 | 162 | Disk also allows you to append a struct or array of structs to a file with data of the same type. 163 | ```swift 164 | try Disk.append(newMessage, to: "messages.json", in: .caches) 165 | ``` 166 | **Note:** you may append a single struct to an empty file, but then in order to properly retrieve that struct again, you must retrieve it as an array. 167 | 168 | **Using custom `JSONEncoder` or `JSONDecoder`** *(Thank you [@nixzhu](https://github.com/saoudrizwan/Disk/pull/16) and [@mecid](https://github.com/saoudrizwan/Disk/pull/28))* 169 | 170 | Behind the scenes, Disk uses Apple's [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder) and [`JSONDecoder`](https://developer.apple.com/documentation/foundation/jsondecoder) classes to encode and decode raw JSON data. You can use custom instances of these classes if you require special encoding or decoding strategies for example. 171 | ```swift 172 | let encoder = JSONEncoder() 173 | encoder.keyEncodingStrategy = .convertToSnakeCase 174 | try Disk.save(messages, to: .caches, as: "messages.json", encoder: encoder) 175 | ``` 176 | ```swift 177 | let decoder = JSONDecoder() 178 | decoder.keyDecodingStrategy = .convertFromSnakeCase 179 | let retrievedMessages = try Disk.retrieve("messages.json", from: .caches, as: [Message].self, decoder: decoder) 180 | ``` 181 | **Note:** appending a `Codable` structure requires Disk to first decode any existing values at the file location, append the new value, then encode the resulting structure to that location. 182 | ```swift 183 | try Disk.append(newMessage, to: "messages.json", in: .caches, decoder: decoder, encoder: encoder) 184 | ``` 185 | 186 | ### Images 187 | ```swift 188 | let image = UIImage(named: "nature.png") 189 | ``` 190 | ```swift 191 | try Disk.save(image, to: .documents, as: "Album/nature.png") 192 | ``` 193 | ```swift 194 | let retrievedImage = try Disk.retrieve("Album/nature.png", from: .documents, as: UIImage.self) 195 | ``` 196 | 197 | **Array of images** 198 | 199 | Multiple images are saved to a new folder. Each image is then named 0.png, 1.png, 2.png, etc. 200 | ```swift 201 | var images = [UIImages]() 202 | // ... 203 | ``` 204 | ```swift 205 | try Disk.save(images, to: .documents, as: "FolderName/") 206 | ``` 207 | You don't need to include the "/" after the folder name, but doing so is declarative that you're not writing all the images' data to one file, but rather as several files to a new folder. 208 | ```swift 209 | let retrievedImages = try Disk.retrieve("FolderName", from: .documents, as: [UIImage].self) 210 | ``` 211 | Let's say you saved a bunch of images to a folder like so: 212 | ```swift 213 | try Disk.save(deer, to: .documents, as: "Nature/deer.png") 214 | try Disk.save(lion, to: .documents, as: "Nature/lion.png") 215 | try Disk.save(bird, to: .documents, as: "Nature/bird.png") 216 | ``` 217 | And maybe you even saved a JSON file to this Nature folder: 218 | ```swift 219 | try Disk.save(diary, to: .documents, as: "Nature/diary.json") 220 | ``` 221 | Then you could retrieve all the images in the Nature folder like so: 222 | ```swift 223 | let images = try Disk.retrieve("Nature", from: .documents, as: [UIImage].self) 224 | ``` 225 | ... which would return `-> [deer.png, lion.png, bird.png]` 226 | 227 | **Appending images** 228 | 229 | Unlike how appending a struct simply modifies an existing JSON file, appending an image adds that image as an independent file to a folder. 230 | ```swift 231 | try Disk.append(goat, to: "Nature", in: .documents) 232 | ``` 233 | **Note:** it's recommended to manually save an independent image using the `save(:to:as:)` function in order to specify a name for that image file in case you want to retrieve it later. Using the `append(:to:in:)` function results in creating a file with an auto-generated name (i.e. if you append an image to a folder with images already present (0.png, 1.png, 2.png), then the new image will be named 3.png.) If the image name is not important, then using `append(:to:in:)` is fine. Appending arrays of images is similar in behavior. 234 | 235 | ### Data 236 | 237 | If you're trying to save data like .mp4 video data for example, then Disk's methods for `Data` will help you work with the file system to persist all data types. 238 | 239 | ```swift 240 | let videoData = Data(contentsOf: videoURL, options: []) 241 | ``` 242 | ```swift 243 | try Disk.save(videoData, to: .documents, as: "anime.mp4") 244 | ``` 245 | ```swift 246 | let retrievedData = try Disk.retrieve("anime.mp4", from: .documents, as: Data.self) 247 | ``` 248 | **Array of `Data`** 249 | 250 | Disk saves arrays of `Data` objects like it does arrays of images, as files in a folder. 251 | ```swift 252 | var data = [Data]() 253 | // ... 254 | ``` 255 | ```swift 256 | try Disk.save(data, to: .documents, as: "videos") 257 | ``` 258 | ```swift 259 | let retrievedVideos = try Disk.retrieve("videos", from: .documents, as: [Data].self) 260 | ``` 261 | If you were to retrieve `[Data]` from a folder with images and JSON files, then those files would be included in the returned value. Continuing the example from the [Array of images](#images) section: 262 | ```swift 263 | let files = try Disk.retrieve("Nature", from: .documents, as: [Data].self) 264 | ``` 265 | ... would return `-> [deer.png, lion.png, bird.png, diary.json]` 266 | 267 | **Appending `Data`** 268 | 269 | Appending `Data` or an array of `Data` is similar to appending an image or array of images—new files are created with auto-generated names and added to the specified folder. 270 | ```swift 271 | try Disk.append(newDataObject, to: "Folder/", in: .documents) 272 | ``` 273 | 274 | ### Large files 275 | It's important to know when to work with the file system on the background thread. Disk is **synchronous**, giving you more control over read/write operations on the file system. [Apple says](https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/TechniquesforReadingandWritingCustomFiles/TechniquesforReadingandWritingCustomFiles.html) that *"because file operations involve accessing the disk, performing those operations **asynchronously** is almost always preferred."* 276 | 277 | [Grand Central Dispatch](https://developer.apple.com/documentation/dispatch) is the best way to work with Disk asynchronously. Here's an example: 278 | ```swift 279 | activityIndicator.startAnimating() 280 | DispatchQueue.global(qos: .userInitiated).async { 281 | do { 282 | try Disk.save(largeData, to: .documents, as: "Movies/spiderman.mp4") 283 | } catch { 284 | // ... 285 | } 286 | DispatchQueue.main.async { 287 | activityIndicator.stopAnimating() 288 | // ... 289 | } 290 | } 291 | ``` 292 | *Don't forget to handle these sorts of tasks [being interrupted](https://stackoverflow.com/a/18305715/3502608).* 293 | 294 | ### iOS 11 Volume Information 295 | Apple introduced several great iOS storage practices in [Session 204](https://developer.apple.com/videos/play/fall2017/204/), putting emphasis on several new `NSURL` volume capacity details added in iOS 11. This information allows us to gauge when it's appropriate to store data on the user's disk. 296 | 297 | * Total capacity 298 | ```swift 299 | Disk.totalCapacity 300 | ``` 301 | 302 | * Available capacity 303 | ```swift 304 | Disk.availableCapacity 305 | ``` 306 | 307 | * Available capacity for important usage. This indicates the amount of space that can be made available for things the user has explicitly requested in the app's UI (i.e. downloading a video or new level for a game.) 308 | ```swift 309 | Disk.availableCapacityForImportantUsage 310 | ``` 311 | 312 | * Available capacity for opportunistic usage. This indicates the amount of space available for things that the user is likely to want but hasn't explicitly requested (i.e. next episode in video series they're watching, or recently updated documents in a server that they might be likely to open.) 313 | ```swift 314 | Disk.availableCapacityForOpportunisticUsage 315 | ``` 316 | 317 | **Note:** These variables return Optional `Int`s since retrieving file system resource values may fail and return `nil`. However this is very unlikely to happen, and this behavior exists solely for safety purposes. 318 | 319 | ### Helper Methods 320 | 321 | * Clear an entire directory 322 | ```swift 323 | try Disk.clear(.caches) 324 | ``` 325 | 326 | * Remove a file/folder 327 | ```swift 328 | try Disk.remove("video.mp4", from: .documents) 329 | ``` 330 | 331 | * Check if file/folder exists 332 | ```swift 333 | if Disk.exists("album", in: .documents) { 334 | // ... 335 | } 336 | ``` 337 | * Move a file/folder to another directory 338 | ```swift 339 | try Disk.move("album/", in: .documents, to: .caches) 340 | ``` 341 | * Rename a file/folder 342 | ```swift 343 | try Disk.rename("currentName.json", in: .documents, to: "newName.json") 344 | ``` 345 | * Get file system URL for a file/folder 346 | ```swift 347 | let url = try Disk.url(for: "album/", in: .documents) 348 | ``` 349 | * Mark a file/folder with the `do not backup` attribute (this keeps the file/folder on disk even in low storage situations, but prevents it from being backed up by iCloud or iTunes.) 350 | ```swift 351 | try Disk.doNotBackup("album", in: .documents) 352 | ``` 353 | > Everything in your app’s home directory is backed up, **with the exception of the application bundle itself, the caches directory, and temporary directory.** 354 | ```swift 355 | try Disk.backup("album", in: .documents) 356 | ``` 357 | (You should generally never use the `.doNotBackup(:in:)` and `.backup(:in:)` methods unless you're absolutely positive you want to persist data no matter what state the user's device is in.) 358 | 359 | #### `URL` Counterparts 360 | Most of these helper methods have `URL` counterparts, in case you want to work with files directly with their file system URLs. 361 | 362 | ```swift 363 | let fileUrl = try Disk.url(for: "file.json", in: .documents) 364 | ``` 365 | 366 | * Remove a file/folder 367 | ```swift 368 | try Disk.remove(fileUrl) 369 | ``` 370 | 371 | * Check if file/folder exists 372 | ```swift 373 | if Disk.exists(fileUrl) { 374 | // ... 375 | } 376 | ``` 377 | * Move a file/folder to another directory 378 | ```swift 379 | let newUrl = try Disk.url(for: "Folder/newFileName.json", in: .documents) 380 | try Disk.move(fileUrl, to: newUrl) 381 | ``` 382 | 383 | * Mark a file/folder with the `do not backup` attribute 384 | ```swift 385 | try Disk.doNotBackup(fileUrl) 386 | ``` 387 | ```swift 388 | try Disk.backup(fileUrl) 389 | ``` 390 | 391 | * Check if URL is of a folder 392 | ```swift 393 | if Disk.isFolder(fileUrl) { 394 | // ... 395 | } 396 | ``` 397 | 398 | ## Debugging 399 | 400 | Disk is *thorough*, meaning that it will not leave an error to chance. Almost all of Disk's methods throw errors either on behalf of `Foundation` or custom Disk Errors that are worth bringing to your attention. These errors have a lot of information, such as a description, failure reason, and recovery suggestion: 401 | ```swift 402 | do { 403 | if Disk.exists("posts.json", in: .documents) { 404 | try Disk.remove("posts.json", from: .documents) 405 | } 406 | } catch let error as NSError { 407 | fatalError(""" 408 | Domain: \(error.domain) 409 | Code: \(error.code) 410 | Description: \(error.localizedDescription) 411 | Failure Reason: \(error.localizedFailureReason ?? "") 412 | Suggestions: \(error.localizedRecoverySuggestion ?? "") 413 | """) 414 | } 415 | ``` 416 | The example above takes care of the most common error when dealing with the file system: removing a file that doesn't exist. 417 | 418 | ## A Word from the Developer 419 | 420 | After developing for iOS for 8+ years, I've come across almost every method of data persistence there is to offer (Core Data, Realm, `NSKeyedArchiver`, `UserDefaults`, etc.) Nothing really fit the bill except `NSKeyedArchiver`, but there were too many hoops to jump through. After Swift 4 was released, I was really excited about the `Codable` protocol because I knew what it had to offer in terms of JSON coding. Working with network responses' JSON data and converting them to usable structures has never been easier. **Disk aims to extend that simplicity of working with data to the file system.** 421 | 422 | Let's say we get some data back from a network request... 423 | ```swift 424 | let _ = URLSession.shared.dataTask(with: request) { (data, response, error) in 425 | DispatchQueue.main.async { 426 | guard error == nil else { fatalError(error!.localizedDescription) } 427 | guard let data = data else { fatalError("No data retrieved") } 428 | 429 | // ... we could directly save this data to disk... 430 | try? Disk.save(data, to: .caches, as: "posts.json") 431 | 432 | } 433 | }.resume() 434 | ``` 435 | ... and retrieve it later as `[Post]`... 436 | ```swift 437 | let posts = try Disk.retrieve("posts.json", from: .caches, as: [Post].self) 438 | ``` 439 | 440 | Disk takes out a lot of the tedious handy work required in coding data to the desired type, and it does it well. Disk also makes necessary but monotonous tasks simple, such as clearing out the caches or temporary directory (as required by Apple's [iOS Data Storage Guidelines](https://developer.apple.com/icloud/documentation/data-storage/index.html)): 441 | 442 | ```swift 443 | try Disk.clear(.temporary) 444 | ``` 445 | Disk is also [significantly faster than alternative persistence solutions like `NSKeyedArchiver`](https://twitter.com/JStheoriginal/status/924810983360434176), since it works directly with the file system. 446 | Best of all, Disk is thorough when it comes to throwing errors, ensuring that you understand why a problem occurs when it does. 447 | 448 | ## Documentation 449 | Option + click on any of Disk's methods for detailed documentation. 450 | documentation 451 | 452 | ## Apps Using Disk 453 | 454 | * [FM Player: Classic DX Synths](https://audiokitpro.com/fm-player-classic-dx-released/) 455 | * [AudioKit Synth One](https://audiokitpro.com/audiokit-synth-one/) 456 | * [BB Links - Your Coaching Links](http://www.bblinksapp.com/) 457 | * [Design+Code Sample App](https://designcode.io/) 458 | * [BookLibrary](https://github.com/saoudrizwan/Disk/issues/67) 459 | * [Nastromy](https://itunes.apple.com/us/app/nastromy/id1444105372?mt=8) 460 | 461 | ## License 462 | 463 | Disk uses the MIT license. Please file an issue if you have any questions or if you'd like to share how you're using Disk. 464 | 465 | ## Contribute 466 | 467 | Please feel free to create issues for feature requests or send pull requests of any additions you think would complement Disk and its philosophy. 468 | 469 | ## Questions? 470 | 471 | Contact me by email hello@saoudmr.com, or by twitter @sdrzn. Please create an issue if you come across a bug or would like a feature to be added. 472 | -------------------------------------------------------------------------------- /Sources/Disk+Codable.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | public extension Disk { 26 | /// Save encodable struct to disk as JSON data 27 | /// 28 | /// - Parameters: 29 | /// - value: the Encodable struct to store 30 | /// - directory: user directory to store the file in 31 | /// - path: file location to store the data (i.e. "Folder/file.json") 32 | /// - encoder: custom JSONEncoder to encode value 33 | /// - Throws: Error if there were any issues encoding the struct or writing it to disk 34 | static func save(_ value: T, to directory: Directory, as path: String, encoder: JSONEncoder = JSONEncoder()) throws { 35 | if path.hasSuffix("/") { 36 | throw createInvalidFileNameForStructsError() 37 | } 38 | do { 39 | let url = try createURL(for: path, in: directory) 40 | let data = try encoder.encode(value) 41 | try createSubfoldersBeforeCreatingFile(at: url) 42 | try data.write(to: url, options: .atomic) 43 | } catch { 44 | throw error 45 | } 46 | } 47 | 48 | /// Append Codable struct JSON data to a file's data 49 | /// 50 | /// - Parameters: 51 | /// - value: the struct to store to disk 52 | /// - path: file location to store the data (i.e. "Folder/file.json") 53 | /// - directory: user directory to store the file in 54 | /// - decoder: custom JSONDecoder to decode existing values 55 | /// - encoder: custom JSONEncoder to encode new value 56 | /// - Throws: Error if there were any issues with encoding/decoding or writing the encoded struct to disk 57 | static func append(_ value: T, to path: String, in directory: Directory, decoder: JSONDecoder = JSONDecoder(), encoder: JSONEncoder = JSONEncoder()) throws { 58 | if path.hasSuffix("/") { 59 | throw createInvalidFileNameForStructsError() 60 | } 61 | do { 62 | if let url = try? getExistingFileURL(for: path, in: directory) { 63 | let oldData = try Data(contentsOf: url) 64 | if !(oldData.count > 0) { 65 | try save([value], to: directory, as: path, encoder: encoder) 66 | } else { 67 | let new: [T] 68 | if let old = try? decoder.decode(T.self, from: oldData) { 69 | new = [old, value] 70 | } else if var old = try? decoder.decode([T].self, from: oldData) { 71 | old.append(value) 72 | new = old 73 | } else { 74 | throw createDeserializationErrorForAppendingStructToInvalidType(url: url, type: value) 75 | } 76 | let newData = try encoder.encode(new) 77 | try newData.write(to: url, options: .atomic) 78 | } 79 | } else { 80 | try save([value], to: directory, as: path, encoder: encoder) 81 | } 82 | } catch { 83 | throw error 84 | } 85 | } 86 | 87 | /// Append Codable struct array JSON data to a file's data 88 | /// 89 | /// - Parameters: 90 | /// - value: the Codable struct array to store 91 | /// - path: file location to store the data (i.e. "Folder/file.json") 92 | /// - directory: user directory to store the file in 93 | /// - decoder: custom JSONDecoder to decode existing values 94 | /// - encoder: custom JSONEncoder to encode new value 95 | /// - Throws: Error if there were any issues writing the encoded struct array to disk 96 | static func append(_ value: [T], to path: String, in directory: Directory, decoder: JSONDecoder = JSONDecoder(), encoder: JSONEncoder = JSONEncoder()) throws { 97 | if path.hasSuffix("/") { 98 | throw createInvalidFileNameForStructsError() 99 | } 100 | do { 101 | if let url = try? getExistingFileURL(for: path, in: directory) { 102 | let oldData = try Data(contentsOf: url) 103 | if !(oldData.count > 0) { 104 | try save(value, to: directory, as: path, encoder: encoder) 105 | } else { 106 | let new: [T] 107 | if let old = try? decoder.decode(T.self, from: oldData) { 108 | new = [old] + value 109 | } else if var old = try? decoder.decode([T].self, from: oldData) { 110 | old.append(contentsOf: value) 111 | new = old 112 | } else { 113 | throw createDeserializationErrorForAppendingStructToInvalidType(url: url, type: value) 114 | } 115 | let newData = try encoder.encode(new) 116 | try newData.write(to: url, options: .atomic) 117 | } 118 | } else { 119 | try save(value, to: directory, as: path, encoder: encoder) 120 | } 121 | } catch { 122 | throw error 123 | } 124 | } 125 | 126 | /// Retrieve and decode a struct from a file on disk 127 | /// 128 | /// - Parameters: 129 | /// - path: path of the file holding desired data 130 | /// - directory: user directory to retrieve the file from 131 | /// - type: struct type (i.e. Message.self or [Message].self) 132 | /// - decoder: custom JSONDecoder to decode existing values 133 | /// - Returns: decoded structs of data 134 | /// - Throws: Error if there were any issues retrieving the data or decoding it to the specified type 135 | static func retrieve(_ path: String, from directory: Directory, as type: T.Type, decoder: JSONDecoder = JSONDecoder()) throws -> T { 136 | if path.hasSuffix("/") { 137 | throw createInvalidFileNameForStructsError() 138 | } 139 | do { 140 | let url = try getExistingFileURL(for: path, in: directory) 141 | let data = try Data(contentsOf: url) 142 | let value = try decoder.decode(type, from: data) 143 | return value 144 | } catch { 145 | throw error 146 | } 147 | } 148 | } 149 | 150 | extension Disk { 151 | /// Helper method to create deserialization error for append(:path:directory:) functions 152 | fileprivate static func createDeserializationErrorForAppendingStructToInvalidType(url: URL, type: T) -> Error { 153 | return Disk.createError( 154 | .deserialization, 155 | description: "Could not deserialize the existing data at \(url.path) to a valid type to append to.", 156 | failureReason: "JSONDecoder could not decode type \(T.self) from the data existing at the file location.", 157 | recoverySuggestion: "Ensure that you only append data structure(s) with the same type as the data existing at the file location.") 158 | } 159 | 160 | /// Helper method to create error for when trying to saving Codable structs as multiple files to a folder 161 | fileprivate static func createInvalidFileNameForStructsError() -> Error { 162 | return Disk.createError( 163 | .invalidFileName, 164 | description: "Cannot save/retrieve the Codable struct without a valid file name. Unlike how arrays of UIImages or Data are stored, Codable structs are not saved as multiple files in a folder, but rather as one JSON file. If you already successfully saved Codable struct(s) to your folder name, try retrieving it as a file named 'Folder' instead of as a folder 'Folder/'", 165 | failureReason: "Disk does not save structs or arrays of structs as multiple files to a folder like it does UIImages or Data.", 166 | recoverySuggestion: "Save your struct or array of structs as one file that encapsulates all the data (i.e. \"multiple-messages.json\")") 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /Sources/Disk+Data.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | public extension Disk { 26 | /// Save Data to disk 27 | /// 28 | /// - Parameters: 29 | /// - value: Data to store to disk 30 | /// - directory: user directory to store the file in 31 | /// - path: file location to store the data (i.e. "Folder/file.mp4") 32 | /// - Throws: Error if there were any issues writing the given data to disk 33 | static func save(_ value: Data, to directory: Directory, as path: String) throws { 34 | do { 35 | let url = try createURL(for: path, in: directory) 36 | try createSubfoldersBeforeCreatingFile(at: url) 37 | try value.write(to: url, options: .atomic) 38 | } catch { 39 | throw error 40 | } 41 | } 42 | 43 | /// Retrieve data from disk 44 | /// 45 | /// - Parameters: 46 | /// - path: path where data file is stored 47 | /// - directory: user directory to retrieve the file from 48 | /// - type: here for Swifty generics magic, use Data.self 49 | /// - Returns: Data retrieved from disk 50 | /// - Throws: Error if there were any issues retrieving the specified file's data 51 | static func retrieve(_ path: String, from directory: Directory, as type: Data.Type) throws -> Data { 52 | do { 53 | let url = try getExistingFileURL(for: path, in: directory) 54 | let data = try Data(contentsOf: url) 55 | return data 56 | } catch { 57 | throw error 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Sources/Disk+Errors.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | extension Disk { 26 | public enum ErrorCode: Int { 27 | case noFileFound = 0 28 | case serialization = 1 29 | case deserialization = 2 30 | case invalidFileName = 3 31 | case couldNotAccessTemporaryDirectory = 4 32 | case couldNotAccessUserDomainMask = 5 33 | case couldNotAccessSharedContainer = 6 34 | } 35 | 36 | public static let errorDomain = "DiskErrorDomain" 37 | 38 | /// Create custom error that FileManager can't account for 39 | static func createError(_ errorCode: ErrorCode, description: String?, failureReason: String?, recoverySuggestion: String?) -> Error { 40 | let errorInfo: [String: Any] = [NSLocalizedDescriptionKey : description ?? "", 41 | NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion ?? "", 42 | NSLocalizedFailureReasonErrorKey: failureReason ?? ""] 43 | return NSError(domain: errorDomain, code: errorCode.rawValue, userInfo: errorInfo) as Error 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/Disk+Helpers.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | public extension Disk { 26 | 27 | /// Get URL for existing file 28 | /// 29 | /// - Parameters: 30 | /// - path: path of file relative to directory (set nil for entire directory) 31 | /// - directory: directory the file is saved in 32 | /// - Returns: URL pointing to file 33 | /// - Throws: Error if no file could be found 34 | @available(*, deprecated, message: "Use Disk.url(for:in:) instead, it does not throw an error if the file does not exist.") 35 | static func getURL(for path: String?, in directory: Directory) throws -> URL { 36 | do { 37 | let url = try getExistingFileURL(for: path, in: directory) 38 | return url 39 | } catch { 40 | throw error 41 | } 42 | } 43 | 44 | /// Construct URL for a potentially existing or non-existent file (Note: replaces `getURL(for:in:)` which would throw an error if file does not exist) 45 | /// 46 | /// - Parameters: 47 | /// - path: path of file relative to directory (set nil for entire directory) 48 | /// - directory: directory for the specified path 49 | /// - Returns: URL for either an existing or non-existing file 50 | /// - Throws: Error if URL creation failed 51 | static func url(for path: String?, in directory: Directory) throws -> URL { 52 | do { 53 | let url = try createURL(for: path, in: directory) 54 | return url 55 | } catch { 56 | throw error 57 | } 58 | } 59 | 60 | /// Clear directory by removing all files 61 | /// 62 | /// - Parameter directory: directory to clear 63 | /// - Throws: Error if FileManager cannot access a directory 64 | static func clear(_ directory: Directory) throws { 65 | do { 66 | let url = try createURL(for: nil, in: directory) 67 | let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) 68 | for fileUrl in contents { 69 | try? FileManager.default.removeItem(at: fileUrl) 70 | } 71 | } catch { 72 | throw error 73 | } 74 | } 75 | 76 | /// Remove file from the file system 77 | /// 78 | /// - Parameters: 79 | /// - path: path of file relative to directory 80 | /// - directory: directory where file is located 81 | /// - Throws: Error if file could not be removed 82 | static func remove(_ path: String, from directory: Directory) throws { 83 | do { 84 | let url = try getExistingFileURL(for: path, in: directory) 85 | try FileManager.default.removeItem(at: url) 86 | } catch { 87 | throw error 88 | } 89 | } 90 | 91 | /// Remove file from the file system 92 | /// 93 | /// - Parameters: 94 | /// - url: URL of file in filesystem 95 | /// - Throws: Error if file could not be removed 96 | static func remove(_ url: URL) throws { 97 | do { 98 | try FileManager.default.removeItem(at: url) 99 | } catch { 100 | throw error 101 | } 102 | } 103 | 104 | /// Checks if a file exists 105 | /// 106 | /// - Parameters: 107 | /// - path: path of file relative to directory 108 | /// - directory: directory where file is located 109 | /// - Returns: Bool indicating whether file exists 110 | static func exists(_ path: String, in directory: Directory) -> Bool { 111 | if let _ = try? getExistingFileURL(for: path, in: directory) { 112 | return true 113 | } 114 | return false 115 | } 116 | 117 | /// Checks if a file exists 118 | /// 119 | /// - Parameters: 120 | /// - url: URL of file in filesystem 121 | /// - Returns: Bool indicating whether file exists 122 | static func exists(_ url: URL) -> Bool { 123 | if FileManager.default.fileExists(atPath: url.path) { 124 | return true 125 | } 126 | return false 127 | } 128 | 129 | /// Sets the 'do not backup' attribute of the file or folder on disk to true. This ensures that the file holding the object data does not get deleted when the user's device has low storage, but prevents this file from being stored in any backups made of the device on iTunes or iCloud. 130 | /// This is only useful for excluding cache and other application support files which are not needed in a backup. Some operations commonly made to user documents will cause the 'do not backup' property to be reset to false and so this should not be used on user documents. 131 | /// Warning: You must ensure that you will purge and handle any files created with this attribute appropriately, as these files will persist on the user's disk even in low storage situtations. If you don't handle these files appropriately, then you aren't following Apple's file system guidlines and can face App Store rejection. 132 | /// Ideally, you should let iOS handle deletion of files in low storage situations, and you yourself handle missing files appropriately (i.e. retrieving an image from the web again if it does not exist on disk anymore.) 133 | /// 134 | /// - Parameters: 135 | /// - path: path of file relative to directory 136 | /// - directory: directory where file is located 137 | /// - Throws: Error if file could not set its 'isExcludedFromBackup' property 138 | static func doNotBackup(_ path: String, in directory: Directory) throws { 139 | do { 140 | try setIsExcludedFromBackup(to: true, for: path, in: directory) 141 | } catch { 142 | throw error 143 | } 144 | } 145 | 146 | /// Sets the 'do not backup' attribute of the file or folder on disk to true. This ensures that the file holding the object data does not get deleted when the user's device has low storage, but prevents this file from being stored in any backups made of the device on iTunes or iCloud. 147 | /// This is only useful for excluding cache and other application support files which are not needed in a backup. Some operations commonly made to user documents will cause the 'do not backup' property to be reset to false and so this should not be used on user documents. 148 | /// Warning: You must ensure that you will purge and handle any files created with this attribute appropriately, as these files will persist on the user's disk even in low storage situtations. If you don't handle these files appropriately, then you aren't following Apple's file system guidlines and can face App Store rejection. 149 | /// Ideally, you should let iOS handle deletion of files in low storage situations, and you yourself handle missing files appropriately (i.e. retrieving an image from the web again if it does not exist on disk anymore.) 150 | /// 151 | /// - Parameters: 152 | /// - url: URL of file in filesystem 153 | /// - Throws: Error if file could not set its 'isExcludedFromBackup' property 154 | static func doNotBackup(_ url: URL) throws { 155 | do { 156 | try setIsExcludedFromBackup(to: true, for: url) 157 | } catch { 158 | throw error 159 | } 160 | } 161 | 162 | /// Sets the 'do not backup' attribute of the file or folder on disk to false. This is the default behaviour so you don't have to use this function unless you already called doNotBackup(name:directory:) on a specific file. 163 | /// This default backing up behaviour allows anything in the .documents and .caches directories to be stored in backups made of the user's device (on iCloud or iTunes) 164 | /// 165 | /// - Parameters: 166 | /// - path: path of file relative to directory 167 | /// - directory: directory where file is located 168 | /// - Throws: Error if file could not set its 'isExcludedFromBackup' property 169 | static func backup(_ path: String, in directory: Directory) throws { 170 | do { 171 | try setIsExcludedFromBackup(to: false, for: path, in: directory) 172 | } catch { 173 | throw error 174 | } 175 | } 176 | 177 | /// Sets the 'do not backup' attribute of the file or folder on disk to false. This is the default behaviour so you don't have to use this function unless you already called doNotBackup(name:directory:) on a specific file. 178 | /// This default backing up behaviour allows anything in the .documents and .caches directories to be stored in backups made of the user's device (on iCloud or iTunes) 179 | /// 180 | /// - Parameters: 181 | /// - url: URL of file in filesystem 182 | /// - Throws: Error if file could not set its 'isExcludedFromBackup' property 183 | static func backup(_ url: URL) throws { 184 | do { 185 | try setIsExcludedFromBackup(to: false, for: url) 186 | } catch { 187 | throw error 188 | } 189 | } 190 | 191 | /// Move file to a new directory 192 | /// 193 | /// - Parameters: 194 | /// - path: path of file relative to directory 195 | /// - directory: directory the file is currently in 196 | /// - newDirectory: new directory to store file in 197 | /// - Throws: Error if file could not be moved 198 | static func move(_ path: String, in directory: Directory, to newDirectory: Directory) throws { 199 | do { 200 | let currentUrl = try getExistingFileURL(for: path, in: directory) 201 | let justDirectoryPath = try createURL(for: nil, in: directory).absoluteString 202 | let filePath = currentUrl.absoluteString.replacingOccurrences(of: justDirectoryPath, with: "") 203 | let newUrl = try createURL(for: filePath, in: newDirectory) 204 | try createSubfoldersBeforeCreatingFile(at: newUrl) 205 | try FileManager.default.moveItem(at: currentUrl, to: newUrl) 206 | } catch { 207 | throw error 208 | } 209 | } 210 | 211 | /// Move file to a new directory 212 | /// 213 | /// - Parameters: 214 | /// - path: path of file relative to directory 215 | /// - directory: directory the file is currently in 216 | /// - newDirectory: new directory to store file in 217 | /// - Throws: Error if file could not be moved 218 | static func move(_ originalURL: URL, to newURL: URL) throws { 219 | do { 220 | try createSubfoldersBeforeCreatingFile(at: newURL) 221 | try FileManager.default.moveItem(at: originalURL, to: newURL) 222 | } catch { 223 | throw error 224 | } 225 | } 226 | 227 | /// Rename a file 228 | /// 229 | /// - Parameters: 230 | /// - path: path of file relative to directory 231 | /// - directory: directory the file is in 232 | /// - newName: new name to give to file 233 | /// - Throws: Error if object could not be renamed 234 | static func rename(_ path: String, in directory: Directory, to newPath: String) throws { 235 | do { 236 | let currentUrl = try getExistingFileURL(for: path, in: directory) 237 | let justDirectoryPath = try createURL(for: nil, in: directory).absoluteString 238 | var currentFilePath = currentUrl.absoluteString.replacingOccurrences(of: justDirectoryPath, with: "") 239 | if isFolder(currentUrl) && currentFilePath.suffix(1) != "/" { 240 | currentFilePath = currentFilePath + "/" 241 | } 242 | let currentValidFilePath = try getValidFilePath(from: path) 243 | let newValidFilePath = try getValidFilePath(from: newPath) 244 | let newFilePath = currentFilePath.replacingOccurrences(of: currentValidFilePath, with: newValidFilePath) 245 | let newUrl = try createURL(for: newFilePath, in: directory) 246 | try createSubfoldersBeforeCreatingFile(at: newUrl) 247 | try FileManager.default.moveItem(at: currentUrl, to: newUrl) 248 | } catch { 249 | throw error 250 | } 251 | } 252 | 253 | /// Check if file at a URL is a folder 254 | static func isFolder(_ url: URL) -> Bool { 255 | var isDirectory: ObjCBool = false 256 | if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) { 257 | if isDirectory.boolValue { 258 | return true 259 | } 260 | } 261 | return false 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /Sources/Disk+InternalHelpers.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | extension Disk { 26 | /// Create and returns a URL constructed from specified directory/path 27 | static func createURL(for path: String?, in directory: Directory) throws -> URL { 28 | let filePrefix = "file://" 29 | var validPath: String? = nil 30 | if let path = path { 31 | do { 32 | validPath = try getValidFilePath(from: path) 33 | } catch { 34 | throw error 35 | } 36 | } 37 | var searchPathDirectory: FileManager.SearchPathDirectory 38 | switch directory { 39 | case .documents: 40 | searchPathDirectory = .documentDirectory 41 | case .caches: 42 | searchPathDirectory = .cachesDirectory 43 | case .applicationSupport: 44 | searchPathDirectory = .applicationSupportDirectory 45 | case .temporary: 46 | if var url = URL(string: NSTemporaryDirectory()) { 47 | if let validPath = validPath { 48 | url = url.appendingPathComponent(validPath, isDirectory: false) 49 | } 50 | if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix { 51 | let fixedUrlString = filePrefix + url.absoluteString 52 | url = URL(string: fixedUrlString)! 53 | } 54 | return url 55 | } else { 56 | throw createError( 57 | .couldNotAccessTemporaryDirectory, 58 | description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")", 59 | failureReason: "Could not get access to the application's temporary directory.", 60 | recoverySuggestion: "Use a different directory." 61 | ) 62 | } 63 | case .sharedContainer(let appGroupName): 64 | if var url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) { 65 | if let validPath = validPath { 66 | url = url.appendingPathComponent(validPath, isDirectory: false) 67 | } 68 | if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix { 69 | let fixedUrl = filePrefix + url.absoluteString 70 | url = URL(string: fixedUrl)! 71 | } 72 | return url 73 | } else { 74 | throw createError( 75 | .couldNotAccessSharedContainer, 76 | description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")", 77 | failureReason: "Could not get access to shared container with app group named \(appGroupName).", 78 | recoverySuggestion: "Check that the app-group name in the entitlement matches the string provided." 79 | ) 80 | } 81 | } 82 | if var url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first { 83 | if let validPath = validPath { 84 | url = url.appendingPathComponent(validPath, isDirectory: false) 85 | } 86 | if url.absoluteString.lowercased().prefix(filePrefix.count) != filePrefix { 87 | let fixedUrlString = filePrefix + url.absoluteString 88 | url = URL(string: fixedUrlString)! 89 | } 90 | return url 91 | } else { 92 | throw createError( 93 | .couldNotAccessUserDomainMask, 94 | description: "Could not create URL for \(directory.pathDescription)/\(validPath ?? "")", 95 | failureReason: "Could not get access to the file system's user domain mask.", 96 | recoverySuggestion: "Use a different directory." 97 | ) 98 | } 99 | } 100 | 101 | /// Find an existing file's URL or throw an error if it doesn't exist 102 | static func getExistingFileURL(for path: String?, in directory: Directory) throws -> URL { 103 | do { 104 | let url = try createURL(for: path, in: directory) 105 | if FileManager.default.fileExists(atPath: url.path) { 106 | return url 107 | } 108 | throw createError( 109 | .noFileFound, 110 | description: "Could not find an existing file or folder at \(url.path).", 111 | failureReason: "There is no existing file or folder at \(url.path)", 112 | recoverySuggestion: "Check if a file or folder exists before trying to commit an operation on it." 113 | ) 114 | } catch { 115 | throw error 116 | } 117 | } 118 | 119 | /// Convert a user generated name to a valid file name 120 | static func getValidFilePath(from originalString: String) throws -> String { 121 | var invalidCharacters = CharacterSet(charactersIn: ":") 122 | invalidCharacters.formUnion(.newlines) 123 | invalidCharacters.formUnion(.illegalCharacters) 124 | invalidCharacters.formUnion(.controlCharacters) 125 | let pathWithoutIllegalCharacters = originalString 126 | .components(separatedBy: invalidCharacters) 127 | .joined(separator: "") 128 | let validFileName = removeSlashesAtBeginning(of: pathWithoutIllegalCharacters) 129 | guard validFileName.count > 0 && validFileName != "." else { 130 | throw createError( 131 | .invalidFileName, 132 | description: "\(originalString) is an invalid file name.", 133 | failureReason: "Cannot write/read a file with the name \(originalString) on disk.", 134 | recoverySuggestion: "Use another file name with alphanumeric characters." 135 | ) 136 | } 137 | return validFileName 138 | } 139 | 140 | /// Helper method for getValidFilePath(from:) to remove all "/" at the beginning of a String 141 | static func removeSlashesAtBeginning(of string: String) -> String { 142 | var string = string 143 | if string.prefix(1) == "/" { 144 | string.remove(at: string.startIndex) 145 | } 146 | if string.prefix(1) == "/" { 147 | string = removeSlashesAtBeginning(of: string) 148 | } 149 | return string 150 | } 151 | 152 | /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system 153 | static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for path: String?, in directory: Directory) throws { 154 | do { 155 | let url = try getExistingFileURL(for: path, in: directory) 156 | var resourceUrl = url 157 | var resourceValues = URLResourceValues() 158 | resourceValues.isExcludedFromBackup = isExcludedFromBackup 159 | try resourceUrl.setResourceValues(resourceValues) 160 | } catch { 161 | throw error 162 | } 163 | } 164 | 165 | /// Set 'isExcludedFromBackup' BOOL property of a file or directory in the file system 166 | static func setIsExcludedFromBackup(to isExcludedFromBackup: Bool, for url: URL) throws { 167 | do { 168 | var resourceUrl = url 169 | var resourceValues = URLResourceValues() 170 | resourceValues.isExcludedFromBackup = isExcludedFromBackup 171 | try resourceUrl.setResourceValues(resourceValues) 172 | } catch { 173 | throw error 174 | } 175 | } 176 | 177 | /// Create necessary sub folders before creating a file 178 | static func createSubfoldersBeforeCreatingFile(at url: URL) throws { 179 | do { 180 | let subfolderUrl = url.deletingLastPathComponent() 181 | var subfolderExists = false 182 | var isDirectory: ObjCBool = false 183 | if FileManager.default.fileExists(atPath: subfolderUrl.path, isDirectory: &isDirectory) { 184 | if isDirectory.boolValue { 185 | subfolderExists = true 186 | } 187 | } 188 | if !subfolderExists { 189 | try FileManager.default.createDirectory(at: subfolderUrl, withIntermediateDirectories: true, attributes: nil) 190 | } 191 | } catch { 192 | throw error 193 | } 194 | } 195 | 196 | /// Get Int from a file name 197 | static func fileNameInt(_ url: URL) -> Int? { 198 | let fileExtension = url.pathExtension 199 | let filePath = url.lastPathComponent 200 | let fileName = filePath.replacingOccurrences(of: fileExtension, with: "") 201 | return Int(String(fileName.filter { "0123456789".contains($0) })) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/Disk+UIImage.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | import UIKit 25 | 26 | public extension Disk { 27 | /// Save image to disk 28 | /// 29 | /// - Parameters: 30 | /// - value: image to store to disk 31 | /// - directory: user directory to store the image file in 32 | /// - path: file location to store the data (i.e. "Folder/file.png") 33 | /// - Throws: Error if there were any issues writing the image to disk 34 | static func save(_ value: UIImage, to directory: Directory, as path: String) throws { 35 | do { 36 | var imageData: Data 37 | if path.suffix(4).lowercased() == ".png" { 38 | let pngData: Data? 39 | #if swift(>=4.2) 40 | pngData = value.pngData() 41 | #else 42 | pngData = UIImagePNGRepresentation(value) 43 | #endif 44 | if let data = pngData { 45 | imageData = data 46 | } else { 47 | throw createError( 48 | .serialization, 49 | description: "Could not serialize UIImage to PNG.", 50 | failureReason: "Data conversion failed.", 51 | recoverySuggestion: "Try saving this image as a .jpg or without an extension at all." 52 | ) 53 | } 54 | } else if path.suffix(4).lowercased() == ".jpg" || path.suffix(5).lowercased() == ".jpeg" { 55 | let jpegData: Data? 56 | #if swift(>=4.2) 57 | jpegData = value.jpegData(compressionQuality: 1) 58 | #else 59 | jpegData = UIImageJPEGRepresentation(value, 1) 60 | #endif 61 | if let data = jpegData { 62 | imageData = data 63 | } else { 64 | throw createError( 65 | .serialization, 66 | description: "Could not serialize UIImage to JPEG.", 67 | failureReason: "Data conversion failed.", 68 | recoverySuggestion: "Try saving this image as a .png or without an extension at all." 69 | ) 70 | } 71 | } else { 72 | var data: Data? 73 | #if swift(>=4.2) 74 | if let pngData = value.pngData() { 75 | data = pngData 76 | } else if let jpegData = value.jpegData(compressionQuality: 1) { 77 | data = jpegData 78 | } 79 | #else 80 | if let pngData = UIImagePNGRepresentation(value) { 81 | data = pngData 82 | } else if let jpegData = UIImageJPEGRepresentation(value, 1) { 83 | data = jpegData 84 | } 85 | #endif 86 | if let data = data { 87 | imageData = data 88 | } else { 89 | throw createError( 90 | .serialization, 91 | description: "Could not serialize UIImage to Data.", 92 | failureReason: "UIImage could not serialize to PNG or JPEG data.", 93 | recoverySuggestion: "Make sure image is not corrupt or try saving without an extension at all." 94 | ) 95 | } 96 | } 97 | let url = try createURL(for: path, in: directory) 98 | try createSubfoldersBeforeCreatingFile(at: url) 99 | try imageData.write(to: url, options: .atomic) 100 | } catch { 101 | throw error 102 | } 103 | } 104 | 105 | /// Retrieve image from disk 106 | /// 107 | /// - Parameters: 108 | /// - path: path where image is stored 109 | /// - directory: user directory to retrieve the image file from 110 | /// - type: here for Swifty generics magic, use UIImage.self 111 | /// - Returns: UIImage from disk 112 | /// - Throws: Error if there were any issues retrieving the specified image 113 | static func retrieve(_ path: String, from directory: Directory, as type: UIImage.Type) throws -> UIImage { 114 | do { 115 | let url = try getExistingFileURL(for: path, in: directory) 116 | let data = try Data(contentsOf: url) 117 | if let image = UIImage(data: data) { 118 | return image 119 | } else { 120 | throw createError( 121 | .deserialization, 122 | description: "Could not decode UIImage from \(url.path).", 123 | failureReason: "A UIImage could not be created out of the data in \(url.path).", 124 | recoverySuggestion: "Try deserializing \(url.path) manually after retrieving it as Data." 125 | ) 126 | } 127 | } catch { 128 | throw error 129 | } 130 | } 131 | } 132 | 133 | 134 | -------------------------------------------------------------------------------- /Sources/Disk+VolumeInformation.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | /// Checking Volume Storage Capacity 26 | /// Confirm that you have enough local storage space for a large amount of data. 27 | /// 28 | /// Source: https://developer.apple.com/documentation/foundation/nsurlresourcekey/checking_volume_storage_capacity?changes=latest_major&language=objc 29 | @available(iOS 11.0, *) 30 | public extension Disk { 31 | /// Helper method to query against a resource value key 32 | private static func getVolumeResourceValues(for key: URLResourceKey) -> URLResourceValues? { 33 | let fileUrl = URL(fileURLWithPath: "/") 34 | let results = try? fileUrl.resourceValues(forKeys: [key]) 35 | return results 36 | } 37 | 38 | /// Volume’s total capacity in bytes. 39 | static var totalCapacity: Int? { 40 | get { 41 | let resourceValues = getVolumeResourceValues(for: .volumeTotalCapacityKey) 42 | return resourceValues?.volumeTotalCapacity 43 | } 44 | } 45 | 46 | /// Volume’s available capacity in bytes. 47 | static var availableCapacity: Int? { 48 | get { 49 | let resourceValues = getVolumeResourceValues(for: .volumeAvailableCapacityKey) 50 | return resourceValues?.volumeAvailableCapacity 51 | } 52 | } 53 | 54 | /// Volume’s available capacity in bytes for storing important resources. 55 | /// 56 | /// Indicates the amount of space that can be made available for things the user has explicitly requested in the app's UI (i.e. downloading a video or new level for a game.) 57 | /// If you need more space than what's available - let user know the request cannot be fulfilled. 58 | static var availableCapacityForImportantUsage: Int? { 59 | get { 60 | let resourceValues = getVolumeResourceValues(for: .volumeAvailableCapacityForImportantUsageKey) 61 | if let result = resourceValues?.volumeAvailableCapacityForImportantUsage { 62 | return Int(exactly: result) 63 | } else { 64 | return nil 65 | } 66 | } 67 | } 68 | 69 | /// Volume’s available capacity in bytes for storing nonessential resources. 70 | /// 71 | /// Indicates the amount of space available for things that the user is likely to want but hasn't explicitly requested (i.e. next episode in video series they're watching, or recently updated documents in a server that they might be likely to open.) 72 | /// For these types of files you might store them initially in the caches directory until they are actually used, at which point you can move them in app support or documents directory. 73 | static var availableCapacityForOpportunisticUsage: Int? { 74 | get { 75 | let resourceValues = getVolumeResourceValues(for: .volumeAvailableCapacityForOpportunisticUsageKey) 76 | if let result = resourceValues?.volumeAvailableCapacityForOpportunisticUsage { 77 | return Int(exactly: result) 78 | } else { 79 | return nil 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Disk+[Data].swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | public extension Disk { 26 | /// Save an array of Data objects to disk 27 | /// 28 | /// - Parameters: 29 | /// - value: array of Data to store to disk 30 | /// - directory: user directory to store the files in 31 | /// - path: folder location to store the data files (i.e. "Folder/") 32 | /// - Throws: Error if there were any issues creating a folder and writing the given [Data] to files in it 33 | static func save(_ value: [Data], to directory: Directory, as path: String) throws { 34 | do { 35 | let folderUrl = try createURL(for: path, in: directory) 36 | try createSubfoldersBeforeCreatingFile(at: folderUrl) 37 | try FileManager.default.createDirectory(at: folderUrl, withIntermediateDirectories: false, attributes: nil) 38 | for i in 0.. largestFileNameInt { 65 | largestFileNameInt = fileNameInt 66 | } 67 | } 68 | } 69 | let newFileNameInt = largestFileNameInt + 1 70 | let data = value 71 | let dataName = "\(newFileNameInt)" 72 | let dataUrl = folderUrl.appendingPathComponent(dataName, isDirectory: false) 73 | try data.write(to: dataUrl, options: .atomic) 74 | } else { 75 | let array = [value] 76 | try save(array, to: directory, as: path) 77 | } 78 | } catch { 79 | throw error 80 | } 81 | } 82 | 83 | /// Append an array of data objects as files to a folder 84 | /// 85 | /// - Parameters: 86 | /// - value: array of Data to store to disk 87 | /// - directory: user directory to create folder with data objects 88 | /// - path: folder location to store the data files (i.e. "Folder/") 89 | /// - Throws: Error if there were any issues writing the given Data 90 | static func append(_ value: [Data], to path: String, in directory: Directory) throws { 91 | do { 92 | if let _ = try? getExistingFileURL(for: path, in: directory) { 93 | for data in value { 94 | try append(data, to: path, in: directory) 95 | } 96 | } else { 97 | try save(value, to: directory, as: path) 98 | } 99 | } catch { 100 | throw error 101 | } 102 | } 103 | 104 | /// Retrieve an array of Data objects from disk 105 | /// 106 | /// - Parameters: 107 | /// - path: path of folder that's holding the Data objects' files 108 | /// - directory: user directory where folder was created for holding Data objects 109 | /// - type: here for Swifty generics magic, use [Data].self 110 | /// - Returns: [Data] from disk 111 | /// - Throws: Error if there were any issues retrieving the specified folder of files 112 | static func retrieve(_ path: String, from directory: Directory, as type: [Data].Type) throws -> [Data] { 113 | do { 114 | let url = try getExistingFileURL(for: path, in: directory) 115 | let fileUrls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) 116 | let sortedFileUrls = fileUrls.sorted(by: { (url1, url2) -> Bool in 117 | if let fileNameInt1 = fileNameInt(url1), let fileNameInt2 = fileNameInt(url2) { 118 | return fileNameInt1 <= fileNameInt2 119 | } 120 | return true 121 | }) 122 | var dataObjects = [Data]() 123 | for i in 0.. 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | import UIKit 25 | 26 | public extension Disk { 27 | /// Save an array of images to disk 28 | /// 29 | /// - Parameters: 30 | /// - value: array of images to store 31 | /// - directory: user directory to store the images in 32 | /// - path: folder location to store the images (i.e. "Folder/") 33 | /// - Throws: Error if there were any issues creating a folder and writing the given images to it 34 | static func save(_ value: [UIImage], to directory: Directory, as path: String) throws { 35 | do { 36 | let folderUrl = try createURL(for: path, in: directory) 37 | try createSubfoldersBeforeCreatingFile(at: folderUrl) 38 | try FileManager.default.createDirectory(at: folderUrl, withIntermediateDirectories: false, attributes: nil) 39 | for i in 0..=4.2) 46 | if let data = image.pngData() { 47 | pngData = data 48 | } else if let data = image.jpegData(compressionQuality: 1) { 49 | jpegData = data 50 | } 51 | #else 52 | if let data = UIImagePNGRepresentation(image) { 53 | pngData = data 54 | } else if let data = UIImageJPEGRepresentation(image, 1) { 55 | jpegData = data 56 | } 57 | #endif 58 | if let data = pngData { 59 | imageData = data 60 | imageName = imageName + ".png" 61 | } else if let data = jpegData { 62 | imageData = data 63 | imageName = imageName + ".jpg" 64 | } else { 65 | throw createError( 66 | .serialization, 67 | description: "Could not serialize UIImage \(i) in the array to Data.", 68 | failureReason: "UIImage \(i) could not serialize to PNG or JPEG data.", 69 | recoverySuggestion: "Make sure there are no corrupt images in the array." 70 | ) 71 | } 72 | let imageUrl = folderUrl.appendingPathComponent(imageName, isDirectory: false) 73 | try imageData.write(to: imageUrl, options: .atomic) 74 | } 75 | } catch { 76 | throw error 77 | } 78 | } 79 | 80 | /// Append an image to a folder 81 | /// 82 | /// - Parameters: 83 | /// - value: image to store to disk 84 | /// - path: folder location to store the image (i.e. "Folder/") 85 | /// - directory: user directory to store the image file in 86 | /// - Throws: Error if there were any issues writing the image to disk 87 | static func append(_ value: UIImage, to path: String, in directory: Directory) throws { 88 | do { 89 | if let folderUrl = try? getExistingFileURL(for: path, in: directory) { 90 | let fileUrls = try FileManager.default.contentsOfDirectory(at: folderUrl, includingPropertiesForKeys: nil, options: []) 91 | var largestFileNameInt = -1 92 | for i in 0.. largestFileNameInt { 96 | largestFileNameInt = fileNameInt 97 | } 98 | } 99 | } 100 | let newFileNameInt = largestFileNameInt + 1 101 | var imageData: Data 102 | var imageName = "\(newFileNameInt)" 103 | var pngData: Data? 104 | var jpegData: Data? 105 | #if swift(>=4.2) 106 | if let data = value.pngData() { 107 | pngData = data 108 | } else if let data = value.jpegData(compressionQuality: 1) { 109 | jpegData = data 110 | } 111 | #else 112 | if let data = UIImagePNGRepresentation(value) { 113 | pngData = data 114 | } else if let data = UIImageJPEGRepresentation(value, 1) { 115 | jpegData = data 116 | } 117 | #endif 118 | if let data = pngData { 119 | imageData = data 120 | imageName = imageName + ".png" 121 | } else if let data = jpegData { 122 | imageData = data 123 | imageName = imageName + ".jpg" 124 | } else { 125 | throw createError( 126 | .serialization, 127 | description: "Could not serialize UIImage to Data.", 128 | failureReason: "UIImage could not serialize to PNG or JPEG data.", 129 | recoverySuggestion: "Make sure image is not corrupt." 130 | ) 131 | } 132 | let imageUrl = folderUrl.appendingPathComponent(imageName, isDirectory: false) 133 | try imageData.write(to: imageUrl, options: .atomic) 134 | } else { 135 | let array = [value] 136 | try save(array, to: directory, as: path) 137 | } 138 | } catch { 139 | throw error 140 | } 141 | } 142 | 143 | /// Append an array of images to a folder 144 | /// 145 | /// - Parameters: 146 | /// - value: images to store to disk 147 | /// - path: folder location to store the images (i.e. "Folder/") 148 | /// - directory: user directory to store the images in 149 | /// - Throws: Error if there were any issues writing the images to disk 150 | static func append(_ value: [UIImage], to path: String, in directory: Directory) throws { 151 | do { 152 | if let _ = try? getExistingFileURL(for: path, in: directory) { 153 | for image in value { 154 | try append(image, to: path, in: directory) 155 | } 156 | } else { 157 | try save(value, to: directory, as: path) 158 | } 159 | } catch { 160 | throw error 161 | } 162 | } 163 | 164 | /// Retrieve an array of images from a folder on disk 165 | /// 166 | /// - Parameters: 167 | /// - path: path of folder holding desired images 168 | /// - directory: user directory where images' folder was created 169 | /// - type: here for Swifty generics magic, use [UIImage].self 170 | /// - Returns: [UIImage] from disk 171 | /// - Throws: Error if there were any issues retrieving the specified folder of images 172 | static func retrieve(_ path: String, from directory: Directory, as type: [UIImage].Type) throws -> [UIImage] { 173 | do { 174 | let url = try getExistingFileURL(for: path, in: directory) 175 | let fileUrls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) 176 | let sortedFileUrls = fileUrls.sorted(by: { (url1, url2) -> Bool in 177 | if let fileNameInt1 = fileNameInt(url1), let fileNameInt2 = fileNameInt(url2) { 178 | return fileNameInt1 <= fileNameInt2 179 | } 180 | return true 181 | }) 182 | var images = [UIImage]() 183 | for i in 0.. 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | #import 24 | 25 | //! Project version number for Disk. 26 | FOUNDATION_EXPORT double DiskVersionNumber; 27 | 28 | //! Project version string for Disk. 29 | FOUNDATION_EXPORT const unsigned char DiskVersionString[]; 30 | 31 | 32 | -------------------------------------------------------------------------------- /Sources/Disk.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2017 Saoud Rizwan 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | /** 26 | 💾 Disk 27 | Easily work with the file system without worrying about any of its intricacies! 28 | 29 | - Save Codable structs, UIImage, [UIImage], Data, [Data] to Apple recommended locations on the user's disk, without having to worry about serialization. 30 | - Retrieve an object from disk as the type you specify, without having to worry about deserialization. 31 | - Remove specific objects from disk, clear entire directories if you need to, check if an object exists on disk, and much more! 32 | - Follow Apple's strict guidelines concerning persistence and using the file system easily. 33 | */ 34 | public class Disk { 35 | fileprivate init() { } 36 | 37 | public enum Directory: Equatable { 38 | /// Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the /Documents directory. 39 | /// Files in this directory are automatically backed up by iCloud. To disable this feature for a specific file, use the .doNotBackup(:in:) method. 40 | case documents 41 | 42 | /// Data that can be downloaded again or regenerated should be stored in the /Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications. 43 | /// Use this directory to write any application-specific support files that you want to persist between launches of the application or during application updates. Your application is generally responsible for adding and removing these files. It should also be able to re-create these files as needed because iTunes removes them during a full restoration of the device. In iOS 2.2 and later, the contents of this directory are not backed up by iTunes. 44 | /// Note that the system may delete the Caches/ directory to free up disk space, so your app must be able to re-create or download these files as needed. 45 | case caches 46 | 47 | /// Put app-created support files in the /Library/Application support directory. In general, this directory includes files that the app uses to run but that should remain hidden from the user. This directory can also include data files, configuration files, templates and modified versions of resources loaded from the app bundle. 48 | /// Files in this directory are automatically backed up by iCloud. To disable this feature for a specific file, use the .doNotBackup(:in:) method. 49 | case applicationSupport 50 | 51 | /// Data that is used only temporarily should be stored in the /tmp directory. Although these files are not backed up to iCloud, remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device. 52 | /// The system will periodically purge these files when your app is not running; therefore, you cannot rely on these files persisting after your app terminates. 53 | case temporary 54 | 55 | /// Sandboxed apps that need to share files with other apps from the same developer on a given device can use a shared container along with the com.apple.security.application-groups entitlement. 56 | /// The shared container or "app group" identifier string is used to locate the corresponding group's shared directory. 57 | /// For more details, visit https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati 58 | case sharedContainer(appGroupName: String) 59 | 60 | public var pathDescription: String { 61 | switch self { 62 | case .documents: return "/Documents" 63 | case .caches: return "/Library/Caches" 64 | case .applicationSupport: return "/Library/Application Support" 65 | case .temporary: return "/tmp" 66 | case .sharedContainer(let appGroupName): return "\(appGroupName)" 67 | } 68 | } 69 | 70 | static public func ==(lhs: Directory, rhs: Directory) -> Bool { 71 | switch (lhs, rhs) { 72 | case (.documents, .documents), (.caches, .caches), (.applicationSupport, .applicationSupport), (.temporary, .temporary): 73 | return true 74 | case (let .sharedContainer(appGroupName: name1), let .sharedContainer(appGroupName: name2)): 75 | return name1 == name2 76 | default: 77 | return false 78 | } 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.6.4 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyAccessedAPITypes 10 | 11 | 12 | NSPrivacyAccessedAPIType 13 | NSPrivacyAccessedAPICategoryDiskSpace 14 | NSPrivacyAccessedAPITypeReasons 15 | 16 | E174.1 17 | 18 | 19 | 20 | NSPrivacyCollectedDataTypes 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Tests/DiskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskTests.swift 3 | // DiskTests 4 | // 5 | // Created by Saoud Rizwan on 7/22/17. 6 | // Copyright © 2017 Saoud Rizwan. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Disk 11 | 12 | class DiskTests: XCTestCase { 13 | 14 | // MARK: Helpers 15 | 16 | // Convert Error -> String of descriptions 17 | func convertErrorToString(_ error: Error) -> String { 18 | return """ 19 | Domain: \((error as NSError).domain) 20 | Code: \((error as NSError).code) 21 | Description: \(error.localizedDescription) 22 | Failure Reason: \((error as NSError).localizedFailureReason ?? "nil") 23 | Suggestions: \((error as NSError).localizedRecoverySuggestion ?? "nil")\n 24 | """ 25 | } 26 | 27 | // We'll clear out all our directories after each test 28 | override func tearDown() { 29 | do { 30 | try Disk.clear(.documents) 31 | try Disk.clear(.caches) 32 | try Disk.clear(.applicationSupport) 33 | try Disk.clear(.temporary) 34 | } catch { 35 | // NOTE: If you get a NSCocoaErrorDomain with code 260, this means one of the above directories could not be found. 36 | // On some of the newer simulators, not all these default directories are initialized at first, but will be created 37 | // after you save something within it. To fix this, run each of the test[directory] test functions below to get each 38 | // respective directory initialized, before running other tests. 39 | fatalError(convertErrorToString(error)) 40 | } 41 | } 42 | 43 | // MARK: Dummmy data 44 | 45 | let messages: [Message] = { 46 | var array = [Message]() 47 | for i in 1...10 { 48 | let element = Message(title: "Message \(i)", body: "...") 49 | array.append(element) 50 | } 51 | return array 52 | }() 53 | 54 | let images = [ 55 | UIImage(named: "Deku", in: Bundle(for: DiskTests.self), compatibleWith: nil)!, 56 | UIImage(named: "AllMight", in: Bundle(for: DiskTests.self), compatibleWith: nil)!, 57 | UIImage(named: "Bakugo", in: Bundle(for: DiskTests.self), compatibleWith: nil)! 58 | ] 59 | 60 | lazy var data: [Data] = self.images.map { $0.pngData()! } 61 | 62 | // MARK: Tests 63 | 64 | func testSaveStructs() { 65 | do { 66 | // 1 struct 67 | try Disk.save(messages[0], to: .documents, as: "message.json") 68 | XCTAssert(Disk.exists("message.json", in: .documents)) 69 | let messageUrl = try Disk.url(for: "message.json", in: .documents) 70 | print("A message was saved as \(messageUrl.absoluteString)") 71 | let retrievedMessage = try Disk.retrieve("message.json", from: .documents, as: Message.self) 72 | XCTAssert(messages[0] == retrievedMessage) 73 | 74 | // ... in folder hierarchy 75 | try Disk.save(messages[0], to: .documents, as: "Messages/Bob/message.json") 76 | XCTAssert(Disk.exists("Messages/Bob/message.json", in: .documents)) 77 | let messageInFolderUrl = try Disk.url(for: "Messages/Bob/message.json", in: .documents) 78 | print("A message was saved as \(messageInFolderUrl.absoluteString)") 79 | let retrievedMessageInFolder = try Disk.retrieve("Messages/Bob/message.json", from: .documents, as: Message.self) 80 | XCTAssert(messages[0] == retrievedMessageInFolder) 81 | 82 | // Array of structs 83 | try Disk.save(messages, to: .documents, as: "messages.json") 84 | XCTAssert(Disk.exists("messages.json", in: .documents)) 85 | let messagesUrl = try Disk.url(for: "messages.json", in: .documents) 86 | print("Messages were saved as \(messagesUrl.absoluteString)") 87 | let retrievedMessages = try Disk.retrieve("messages.json", from: .documents, as: [Message].self) 88 | XCTAssert(messages == retrievedMessages) 89 | } catch { 90 | fatalError(convertErrorToString(error)) 91 | } 92 | } 93 | 94 | func testAppendStructs() { 95 | do { 96 | // Append a single struct to an empty location 97 | try Disk.append(messages[0], to: "single-message.json", in: .documents) 98 | let retrievedSingleMessage = try Disk.retrieve("single-message.json", from: .documents, as: [Message].self) 99 | XCTAssert(Disk.exists("single-message.json", in: .documents)) 100 | XCTAssert(retrievedSingleMessage[0] == messages[0]) 101 | 102 | // Append an array of structs to an empty location 103 | try Disk.append(messages, to: "multiple-messages.json", in: .documents) 104 | let retrievedMultipleMessages = try Disk.retrieve("multiple-messages.json", from: .documents, as: [Message].self) 105 | XCTAssert(Disk.exists("multiple-messages.json", in: .documents)) 106 | XCTAssert(retrievedMultipleMessages == messages) 107 | 108 | // Append a single struct to a single struct 109 | try Disk.save(messages[0], to: .documents, as: "messages.json") 110 | XCTAssert(Disk.exists("messages.json", in: .documents)) 111 | try Disk.append(messages[1], to: "messages.json", in: .documents) 112 | let retrievedMessages = try Disk.retrieve("messages.json", from: .documents, as: [Message].self) 113 | XCTAssert(retrievedMessages[0] == messages[0] && retrievedMessages[1] == messages[1]) 114 | 115 | // Append an array of structs to a single struct 116 | try Disk.save(messages[5], to: .caches, as: "one-message.json") 117 | try Disk.append(messages, to: "one-message.json", in: .caches) 118 | let retrievedOneMessage = try Disk.retrieve("one-message.json", from: .caches, as: [Message].self) 119 | XCTAssert(retrievedOneMessage.count == messages.count + 1) 120 | XCTAssert(retrievedOneMessage[0] == messages[5]) 121 | XCTAssert(retrievedOneMessage.last! == messages.last!) 122 | 123 | // Append a single struct to an array of structs 124 | try Disk.save(messages, to: .documents, as: "many-messages.json") 125 | try Disk.append(messages[1], to: "many-messages.json", in: .documents) 126 | let retrievedManyMessages = try Disk.retrieve("many-messages.json", from: .documents, as: [Message].self) 127 | XCTAssert(retrievedManyMessages.count == messages.count + 1) 128 | XCTAssert(retrievedManyMessages[0] == messages[0]) 129 | XCTAssert(retrievedManyMessages.last! == messages[1]) 130 | 131 | let array = [messages[0], messages[1], messages[2]] 132 | try Disk.save(array, to: .documents, as: "a-few-messages.json") 133 | XCTAssert(Disk.exists("a-few-messages.json", in: .documents)) 134 | try Disk.append(messages[3], to: "a-few-messages.json", in: .documents) 135 | let retrievedFewMessages = try Disk.retrieve("a-few-messages.json", from: .documents, as: [Message].self) 136 | XCTAssert(retrievedFewMessages[0] == array[0] && retrievedFewMessages[1] == array[1] && retrievedFewMessages[2] == array[2] && retrievedFewMessages[3] == messages[3]) 137 | 138 | // Append an array of structs to an array of structs 139 | try Disk.save(messages, to: .documents, as: "array-of-structs.json") 140 | try Disk.append(messages, to: "array-of-structs.json", in: .documents) 141 | let retrievedArrayOfStructs = try Disk.retrieve("array-of-structs.json", from: .documents, as: [Message].self) 142 | XCTAssert(retrievedArrayOfStructs.count == (messages.count * 2)) 143 | XCTAssert(retrievedArrayOfStructs[0] == messages[0] && retrievedArrayOfStructs.last! == messages.last!) 144 | } catch { 145 | fatalError(convertErrorToString(error)) 146 | } 147 | } 148 | 149 | func testSaveImages() { 150 | do { 151 | // 1 image 152 | try Disk.save(images[0], to: .documents, as: "image.png") 153 | XCTAssert(Disk.exists("image.png", in: .documents)) 154 | let imageUrl = try Disk.url(for: "image.png", in: .documents) 155 | print("An image was saved as \(imageUrl.absoluteString)") 156 | let retrievedImage = try Disk.retrieve("image.png", from: .documents, as: UIImage.self) 157 | XCTAssert(images[0].dataEquals(retrievedImage)) 158 | 159 | // ... in folder hierarchy 160 | try Disk.save(images[0], to: .documents, as: "Photos/image.png") 161 | XCTAssert(Disk.exists("Photos/image.png", in: .documents)) 162 | let imageInFolderUrl = try Disk.url(for: "Photos/image.png", in: .documents) 163 | print("An image was saved as \(imageInFolderUrl.absoluteString)") 164 | let retrievedInFolderImage = try Disk.retrieve("Photos/image.png", from: .documents, as: UIImage.self) 165 | XCTAssert(images[0].dataEquals(retrievedInFolderImage)) 166 | 167 | // Array of images 168 | try Disk.save(images, to: .documents, as: "album/") 169 | XCTAssert(Disk.exists("album/", in: .documents)) 170 | let imagesFolderUrl = try Disk.url(for: "album/", in: .documents) 171 | print("Images were saved as \(imagesFolderUrl.absoluteString)") 172 | let retrievedImages = try Disk.retrieve("album/", from: .documents, as: [UIImage].self) 173 | for i in 0.. [Data] 322 | try Disk.save(arrayOfImagesData, to: .documents, as: "data-folder/") 323 | XCTAssert(Disk.exists("data-folder/", in: .documents)) 324 | let folderUrl = try Disk.url(for: "data-folder/", in: .documents) 325 | print("Files were saved to \(folderUrl.absoluteString)") 326 | // Retrieve the files as [UIImage] 327 | let retrievedFilesAsImages = try Disk.retrieve("data-folder/", from: .documents, as: [UIImage].self) 328 | for i in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | -------------------------------------------------------------------------------- /Tests/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message.swift 3 | // DiskTests 4 | // 5 | // Created by Saoud Rizwan on 7/22/17. 6 | // Copyright © 2017 Saoud Rizwan. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Message: Codable { 12 | let title: String 13 | let body: String 14 | } 15 | 16 | // Conforms to Equatable so we can compare messages (i.e. message1 == message2) 17 | extension Message: Equatable { 18 | static func == (lhs: Message, rhs: Message) -> Bool { 19 | return lhs.title == rhs.title && lhs.body == rhs.body 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // Disk 4 | // 5 | // Created by Saoud Rizwan on 8/21/17. 6 | // Copyright © 2017 Saoud Rizwan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // UIImage's current Equatable implementation is buggy, this is a simply workaround to compare images' Data 12 | extension UIImage { 13 | func dataEquals(_ otherImage: UIImage) -> Bool { 14 | if let selfData = self.pngData(), let otherData = otherImage.pngData() { 15 | return selfData == otherData 16 | } else { 17 | print("Could not convert images to PNG") 18 | return false 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------