commit
3e216515b7
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
.build
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -25,3 +25,9 @@
|
||||
[submodule "Cores/DSDeltaCore"]
|
||||
path = Cores/DSDeltaCore
|
||||
url = https://github.com/rileytestut/DSDeltaCore.git
|
||||
[submodule "Cores/MelonDSDeltaCore"]
|
||||
path = Cores/MelonDSDeltaCore
|
||||
url = https://github.com/rileytestut/MelonDSDeltaCore.git
|
||||
[submodule "Cores/GPGXDeltaCore"]
|
||||
path = Cores/GPGXDeltaCore
|
||||
url = https://github.com/rileytestut/GPGXDeltaCore.git
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit 40d515a2088be952b6c4038ff715d1b91ce0667f
|
||||
Subproject commit 1054482de95f60de78c48b01af3c04fb4eca9168
|
||||
@ -1 +1 @@
|
||||
Subproject commit bcedcc293badfc9e4f07f3c66f496bf828450874
|
||||
Subproject commit e2b3f0e46b4c64670e13fd0466ebdac719f84555
|
||||
@ -1 +1 @@
|
||||
Subproject commit 14474b02ec607953cdc6dd3959856f7de1449bdb
|
||||
Subproject commit 8523e03358559cebaa36b67bd0a87698df238512
|
||||
@ -1 +1 @@
|
||||
Subproject commit 85ced43f5f220ee7f19b99e0949d9091c963d549
|
||||
Subproject commit 4313fa6670ab534e70d13532c2504761f849c432
|
||||
1
Cores/GPGXDeltaCore
Submodule
1
Cores/GPGXDeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit ad4ba365acf1800d372ebfaa98df86b9c1b23dce
|
||||
1
Cores/MelonDSDeltaCore
Submodule
1
Cores/MelonDSDeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit d6b33d89043898d521546d9062d12a505b7d2101
|
||||
@ -1 +1 @@
|
||||
Subproject commit 9057ad53e99555c75de3b4083fc4c18de13254ce
|
||||
Subproject commit e598f512b498e1b639a8d2134113169f4b8d0d26
|
||||
@ -1 +1 @@
|
||||
Subproject commit 9d1a964c1b23f8c2835d469d2827d4f24bcc5214
|
||||
Subproject commit 78fa7db707655962a1077f4681c35fcf81510060
|
||||
@ -1 +1 @@
|
||||
Subproject commit f54e0440f1a9ed1fc005f5ea4729ebe26f36f945
|
||||
Subproject commit 7539cbaac26a3d2ca9daf47ba22d1b0ebbc41a2b
|
||||
@ -18,26 +18,32 @@
|
||||
name = mogenerator;
|
||||
productName = mogenerator;
|
||||
};
|
||||
BF6E70B925D2187800E41CD1 /* Systems */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = BF6E70BA25D2187800E41CD1 /* Build configuration list for PBXAggregateTarget "Systems" */;
|
||||
buildPhases = (
|
||||
BF6E70BD25D2187F00E41CD1 /* ShellScript */,
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Systems;
|
||||
productName = Systems;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
4FE8465FD28810191C3E5212 /* Pods_Delta.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 695E8C07A709B16AAD83AEC8 /* Pods_Delta.framework */; };
|
||||
1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; };
|
||||
BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF00BEA525B758AA00C8607D /* SystemBIOS.swift */; };
|
||||
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */; };
|
||||
BF0418141D01E93400E85BCF /* GBADeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF0418131D01E93400E85BCF /* GBADeltaCore.framework */; };
|
||||
BF0418151D01E93400E85BCF /* GBADeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF0418131D01E93400E85BCF /* GBADeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; };
|
||||
BF072010219A3A9D00F05DA4 /* ZIPFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF07200E219A3A9500F05DA4 /* ZIPFoundation.framework */; };
|
||||
BF072011219A3A9D00F05DA4 /* ZIPFoundation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF07200E219A3A9500F05DA4 /* ZIPFoundation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF1020E31F95B05B00313182 /* DeltaToDelta2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF1020E21F95B05B00313182 /* DeltaToDelta2.xcmappingmodel */; };
|
||||
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; };
|
||||
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */; };
|
||||
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
|
||||
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
|
||||
BF144C642238511400C387E1 /* Harmony_Dropbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */; };
|
||||
BF144C652238511400C387E1 /* Harmony_Dropbox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF15AF831F54B43B009B6AAB /* ActionInput.swift */; };
|
||||
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; };
|
||||
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */; };
|
||||
BF1DAD5D1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift */; };
|
||||
BF1F45A421AF274D00EF9895 /* SyncResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */; };
|
||||
BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */; };
|
||||
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */; };
|
||||
@ -60,10 +66,6 @@
|
||||
BF4828861F9028F500028B97 /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4828851F9028F500028B97 /* System.swift */; };
|
||||
BF4828881F90290F00028B97 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4828871F90290F00028B97 /* Action.swift */; };
|
||||
BF48F74E219A16DA00BC2FC1 /* SyncingServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF48F74D219A16DA00BC2FC1 /* SyncingServicesViewController.swift */; };
|
||||
BF48F755219A1EF000BC2FC1 /* Harmony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF48F754219A1EEB00BC2FC1 /* Harmony.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
BF48F756219A1EF000BC2FC1 /* Harmony.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF48F754219A1EEB00BC2FC1 /* Harmony.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF48F75B219A1F8A00BC2FC1 /* Harmony_Drive.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF48F75A219A1F8300BC2FC1 /* Harmony_Drive.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
BF48F75C219A1F8A00BC2FC1 /* Harmony_Drive.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF48F75A219A1F8300BC2FC1 /* Harmony_Drive.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF525EE81FF5F370004AA849 /* DeepLinkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF525EE71FF5F370004AA849 /* DeepLinkController.swift */; };
|
||||
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF525EE91FF6CD12004AA849 /* DeepLink.swift */; };
|
||||
BF56450D220239B800A8EA26 /* GameControllerInputMappingMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56450C220239B800A8EA26 /* GameControllerInputMappingMigrationPolicy.swift */; };
|
||||
@ -96,6 +98,10 @@
|
||||
BF647A6A22FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */; };
|
||||
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */; };
|
||||
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF696B7F1D9B2B02009639E0 /* Theme.swift */; };
|
||||
BF69FBA223E375A20051BEEA /* libVBA-M.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBA123E375A20051BEEA /* libVBA-M.a */; };
|
||||
BF69FBA823E396860051BEEA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBA723E3967B0051BEEA /* libz.tbd */; };
|
||||
BF69FBAA23E399AA0051BEEA /* CoreMotion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBA923E399AA0051BEEA /* CoreMotion.framework */; };
|
||||
BF69FBC923E3A8380051BEEA /* libNestopia.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBC823E3A8380051BEEA /* libNestopia.a */; };
|
||||
BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3121EB7E47F008E83CD /* ImportOption.swift */; };
|
||||
BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */; };
|
||||
BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */; };
|
||||
@ -108,7 +114,6 @@
|
||||
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF71CF861FE90006001F1613 /* AppIconShortcutsViewController.swift */; };
|
||||
BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */; };
|
||||
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; };
|
||||
BF79966E224C076C009B094F /* N64DeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF79966C224C075A009B094F /* N64DeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */; };
|
||||
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */; };
|
||||
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */; };
|
||||
@ -118,48 +123,34 @@
|
||||
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */; };
|
||||
BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2761E4977BF0030E7AD /* GameMetadata.swift */; };
|
||||
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */; };
|
||||
BF98C9822204D9AB006B95AC /* NESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF98C9812204D9A1006B95AC /* NESDeltaCore.framework */; };
|
||||
BF98C9832204D9AB006B95AC /* NESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF98C9812204D9A1006B95AC /* NESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */; };
|
||||
BF99C6941D0A9AA600BA92BC /* SNESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; };
|
||||
BF99C6951D0A9AA600BA92BC /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF9F4FCF1AAD7B87004C9500 /* DeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; };
|
||||
BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63BDE91D389EEB00FCB040 /* GameViewController.swift */; };
|
||||
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; };
|
||||
BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAB9F7C219A43380080EC7D /* SyncManager.swift */; };
|
||||
BFAB9F88219A4B670080EC7D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = BFAB9F87219A4B670080EC7D /* GoogleService-Info.plist */; };
|
||||
BFB3593A2278FD0000CFD920 /* N64DeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF79966C224C075A009B094F /* N64DeltaCore.framework */; };
|
||||
BFB359432278FD7E00CFD920 /* N64DeltaCore_Video.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB359412278FD6700CFD920 /* N64DeltaCore_Video.framework */; };
|
||||
BFB359442278FD7E00CFD920 /* N64DeltaCore_Video.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFB359412278FD6700CFD920 /* N64DeltaCore_Video.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFB359452278FD8100CFD920 /* N64DeltaCore_RSP.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB359422278FD6800CFD920 /* N64DeltaCore_RSP.framework */; };
|
||||
BFB359462278FD8100CFD920 /* N64DeltaCore_RSP.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFB359422278FD6800CFD920 /* N64DeltaCore_RSP.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFB3645823245A6000CD0EB1 /* LicensesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB3645723245A6000CD0EB1 /* LicensesViewController.swift */; };
|
||||
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */; };
|
||||
BFC1F2CC22F9515F00606A45 /* CopyDeepLinkActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F2CB22F9515F00606A45 /* CopyDeepLinkActivity.swift */; };
|
||||
BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */; };
|
||||
BFC6F7B81F435BC500221B96 /* Input+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC6F7B71F435BC500221B96 /* Input+Display.swift */; };
|
||||
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */; };
|
||||
BFCADF1E25D22FE2008D78FB /* Systems.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF6E70C225D22F7600E41CD1 /* Systems.framework */; };
|
||||
BFCADF1F25D22FE2008D78FB /* Systems.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF6E70C225D22F7600E41CD1 /* Systems.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */; };
|
||||
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; };
|
||||
BFD1EF402336BD8800D197CF /* UIDevice+Processor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */; };
|
||||
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB3417219E4B1700595A62 /* SyncStatusViewController.swift */; };
|
||||
BFDCA1E6244EBAA900B8FBDB /* liblibDeSmuME.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDCA1E5244EBAA900B8FBDB /* liblibDeSmuME.a */; };
|
||||
BFDCA1E9244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BFDCA1E8244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel */; };
|
||||
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; };
|
||||
BFDE2CD1222DF36A008038E0 /* SwiftyDropbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */; };
|
||||
BFDE2CD2222DF36A008038E0 /* SwiftyDropbox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFDE2CD3222DF36A008038E0 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC7222DF345008038E0 /* Alamofire.framework */; };
|
||||
BFDE2CD4222DF36A008038E0 /* Alamofire.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC7222DF345008038E0 /* Alamofire.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFDF71DD22F94CF70074D92E /* DSDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDF71DA22F94CDF0074D92E /* DSDeltaCore.framework */; };
|
||||
BFDF71DE22F94CF70074D92E /* DSDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDF71DA22F94CDF0074D92E /* DSDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; };
|
||||
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
|
||||
BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */; };
|
||||
BFE593CA21F3F8B7003412A6 /* GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593C921F3F8B7003412A6 /* GameSave.swift */; };
|
||||
BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593CB21F3F8C2003412A6 /* _GameSave.swift */; };
|
||||
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; };
|
||||
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFE9908024451E15006409A7 /* MelonDSCoreSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE9907F24451E15006409A7 /* MelonDSCoreSettingsViewController.swift */; };
|
||||
BFEE943D23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943C23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel */; };
|
||||
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */; };
|
||||
BFF0742C1E9DC17500ACDF4A /* GBCDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */; };
|
||||
BFF0742D1E9DC17500ACDF4A /* GBCDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFF6452E1F7CC5060056533E /* GameControllerInputMappingTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */; };
|
||||
BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */; };
|
||||
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; };
|
||||
@ -171,31 +162,29 @@
|
||||
BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC46221D5984A000AF2CC6 /* LaunchViewController.swift */; };
|
||||
BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFC46441D59861000AF2CC6 /* LaunchScreen.storyboard */; };
|
||||
BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC464B1D5998D600AF2CC6 /* CheatTableViewCell.swift */; };
|
||||
BFFDF03723E3BB2600931B96 /* libSnes9x.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03623E3BB2600931B96 /* libSnes9x.a */; };
|
||||
BFFDF03F23E3C28A00931B96 /* libGambatte.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03D23E3C0F000931B96 /* libGambatte.a */; };
|
||||
BFFDF04623E3D3A600931B96 /* libMupen64Plus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
BFEBE58225D3388F00222319 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BFFA71CF1AAC406100EE9DD1 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = BF6E70B925D2187800E41CD1;
|
||||
remoteInfo = Systems;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
BF9F4FCC1AAD7AEE004C9500 /* Embed Frameworks */ = {
|
||||
BF08DC3325CE07C3007A9CF4 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */,
|
||||
BFDE2CD2222DF36A008038E0 /* SwiftyDropbox.framework in Embed Frameworks */,
|
||||
BF48F75C219A1F8A00BC2FC1 /* Harmony_Drive.framework in Embed Frameworks */,
|
||||
BF79966E224C076C009B094F /* N64DeltaCore.framework in Embed Frameworks */,
|
||||
BF48F756219A1EF000BC2FC1 /* Harmony.framework in Embed Frameworks */,
|
||||
BFB359462278FD8100CFD920 /* N64DeltaCore_RSP.framework in Embed Frameworks */,
|
||||
BFF0742D1E9DC17500ACDF4A /* GBCDeltaCore.framework in Embed Frameworks */,
|
||||
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */,
|
||||
BF0418151D01E93400E85BCF /* GBADeltaCore.framework in Embed Frameworks */,
|
||||
BFDE2CD4222DF36A008038E0 /* Alamofire.framework in Embed Frameworks */,
|
||||
BFB359442278FD7E00CFD920 /* N64DeltaCore_Video.framework in Embed Frameworks */,
|
||||
BFDF71DE22F94CF70074D92E /* DSDeltaCore.framework in Embed Frameworks */,
|
||||
BF144C652238511400C387E1 /* Harmony_Dropbox.framework in Embed Frameworks */,
|
||||
BF98C9832204D9AB006B95AC /* NESDeltaCore.framework in Embed Frameworks */,
|
||||
BF99C6951D0A9AA600BA92BC /* SNESDeltaCore.framework in Embed Frameworks */,
|
||||
BF072011219A3A9D00F05DA4 /* ZIPFoundation.framework in Embed Frameworks */,
|
||||
BFCADF1F25D22FE2008D78FB /* Systems.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -204,8 +193,8 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
22506DA00971C4300AF90A35 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
695E8C07A709B16AAD83AEC8 /* Pods_Delta.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Delta.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = "<group>"; };
|
||||
BF00BEA525B758AA00C8607D /* SystemBIOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemBIOS.swift; sourceTree = "<group>"; };
|
||||
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = openvgdb.sqlite; sourceTree = "<group>"; };
|
||||
BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkinsViewController.swift; sourceTree = "<group>"; };
|
||||
@ -219,7 +208,7 @@
|
||||
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = "<group>"; };
|
||||
BF15AF831F54B43B009B6AAB /* ActionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionInput.swift; sourceTree = "<group>"; };
|
||||
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = "<group>"; };
|
||||
BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemControllerSkinsViewController.swift; sourceTree = "<group>"; };
|
||||
BF1DAD5C1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferredControllerSkinsViewController.swift; sourceTree = "<group>"; };
|
||||
BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncResultViewController.swift; sourceTree = "<group>"; };
|
||||
BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SyncResultsViewController.storyboard; sourceTree = "<group>"; };
|
||||
BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HarmonyMetadataKey+Keys.swift"; sourceTree = "<group>"; };
|
||||
@ -228,6 +217,7 @@
|
||||
BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = "<group>"; };
|
||||
BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
|
||||
BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesCollectionHeaderView.swift; sourceTree = "<group>"; };
|
||||
BF30AC25244E88BE00F0C744 /* libMelonDSDeltaCore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libMelonDSDeltaCore.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF31878A1D489AAA00BD020D /* CheatValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatValidator.swift; sourceTree = "<group>"; };
|
||||
BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditCheatViewController.swift; sourceTree = "<group>"; };
|
||||
BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatTextView.swift; sourceTree = "<group>"; };
|
||||
@ -274,6 +264,7 @@
|
||||
BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFetchedResultsController+Conveniences.m"; sourceTree = "<group>"; };
|
||||
BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Conveniences.swift"; sourceTree = "<group>"; };
|
||||
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
|
||||
BF5ACE3823E23D6500BD0F20 /* libVBA-M.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libVBA-M.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Settings.storyboard; path = Delta/Base.lproj/Settings.storyboard; sourceTree = SOURCE_ROOT; };
|
||||
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerInputsViewController.swift; sourceTree = "<group>"; };
|
||||
@ -285,6 +276,10 @@
|
||||
BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+SwizzleBundleID.swift"; sourceTree = "<group>"; };
|
||||
BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ControllerSkin+Configuring.swift"; sourceTree = "<group>"; };
|
||||
BF696B7F1D9B2B02009639E0 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
BF69FBA123E375A20051BEEA /* libVBA-M.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libVBA-M.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF69FBA723E3967B0051BEEA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||
BF69FBA923E399AA0051BEEA /* CoreMotion.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMotion.framework; path = System/Library/Frameworks/CoreMotion.framework; sourceTree = SDKROOT; };
|
||||
BF69FBC823E3A8380051BEEA /* libNestopia.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libNestopia.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMappingTransformer.swift; sourceTree = "<group>"; };
|
||||
BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
BF6BF3121EB7E47F008E83CD /* ImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportOption.swift; sourceTree = "<group>"; };
|
||||
@ -293,6 +288,7 @@
|
||||
BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabaseImportOption.swift; sourceTree = "<group>"; };
|
||||
BF6BF3201EB82362008E83CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/GamesDatabase.storyboard; sourceTree = "<group>"; };
|
||||
BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibraryImportOption.swift; sourceTree = "<group>"; };
|
||||
BF6E70C225D22F7600E41CD1 /* Systems.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Systems.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _GameControllerInputMapping.swift; sourceTree = "<group>"; };
|
||||
BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMapping.swift; sourceTree = "<group>"; };
|
||||
BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -327,6 +323,9 @@
|
||||
BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewControllerContextTransitioning+Conveniences.swift"; sourceTree = "<group>"; };
|
||||
BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Processor.swift"; sourceTree = "<group>"; };
|
||||
BFDB3417219E4B1700595A62 /* SyncStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusViewController.swift; sourceTree = "<group>"; };
|
||||
BFDCA1E5244EBAA900B8FBDB /* liblibDeSmuME.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblibDeSmuME.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFDCA1E7244F7DB100B8FBDB /* Delta 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 6.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BFDCA1E8244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta5ToDelta6.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Harmony_Dropbox.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFDE2CC7222DF345008038E0 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -334,9 +333,13 @@
|
||||
BFDF71DA22F94CDF0074D92E /* DSDeltaCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = DSDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuButton.swift; sourceTree = "<group>"; };
|
||||
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
|
||||
BFE4275223EDF75300E6B417 /* Delta 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 5.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SymbolFallback.swift"; sourceTree = "<group>"; };
|
||||
BFE593C921F3F8B7003412A6 /* GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSave.swift; sourceTree = "<group>"; };
|
||||
BFE593CB21F3F8C2003412A6 /* _GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _GameSave.swift; sourceTree = "<group>"; };
|
||||
BFE9907F24451E15006409A7 /* MelonDSCoreSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MelonDSCoreSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFEE943C23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta4ToDelta5.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveStateMigrationPolicy.swift; sourceTree = "<group>"; };
|
||||
BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBCDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameTableViewCell.swift; sourceTree = "<group>"; };
|
||||
@ -351,7 +354,11 @@
|
||||
BFFC46221D5984A000AF2CC6 /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
|
||||
BFFC46451D59861000AF2CC6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
BFFC464B1D5998D600AF2CC6 /* CheatTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatTableViewCell.swift; sourceTree = "<group>"; };
|
||||
BFFDF03623E3BB2600931B96 /* libSnes9x.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libSnes9x.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFFDF03D23E3C0F000931B96 /* libGambatte.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libGambatte.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libMupen64Plus.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -359,23 +366,16 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFB3593A2278FD0000CFD920 /* N64DeltaCore.framework in Frameworks */,
|
||||
BF9F4FCF1AAD7B87004C9500 /* DeltaCore.framework in Frameworks */,
|
||||
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */,
|
||||
BFDE2CD3222DF36A008038E0 /* Alamofire.framework in Frameworks */,
|
||||
BF48F75B219A1F8A00BC2FC1 /* Harmony_Drive.framework in Frameworks */,
|
||||
BFDF71DD22F94CF70074D92E /* DSDeltaCore.framework in Frameworks */,
|
||||
BFB359452278FD8100CFD920 /* N64DeltaCore_RSP.framework in Frameworks */,
|
||||
BF99C6941D0A9AA600BA92BC /* SNESDeltaCore.framework in Frameworks */,
|
||||
BF98C9822204D9AB006B95AC /* NESDeltaCore.framework in Frameworks */,
|
||||
BFB359432278FD7E00CFD920 /* N64DeltaCore_Video.framework in Frameworks */,
|
||||
BF0418141D01E93400E85BCF /* GBADeltaCore.framework in Frameworks */,
|
||||
BF072010219A3A9D00F05DA4 /* ZIPFoundation.framework in Frameworks */,
|
||||
BFDE2CD1222DF36A008038E0 /* SwiftyDropbox.framework in Frameworks */,
|
||||
BF144C642238511400C387E1 /* Harmony_Dropbox.framework in Frameworks */,
|
||||
BFF0742C1E9DC17500ACDF4A /* GBCDeltaCore.framework in Frameworks */,
|
||||
BF48F755219A1EF000BC2FC1 /* Harmony.framework in Frameworks */,
|
||||
4FE8465FD28810191C3E5212 /* Pods_Delta.framework in Frameworks */,
|
||||
BF69FBA823E396860051BEEA /* libz.tbd in Frameworks */,
|
||||
BF69FBAA23E399AA0051BEEA /* CoreMotion.framework in Frameworks */,
|
||||
BF69FBA223E375A20051BEEA /* libVBA-M.a in Frameworks */,
|
||||
BFFDF04623E3D3A600931B96 /* libMupen64Plus.a in Frameworks */,
|
||||
BFDCA1E6244EBAA900B8FBDB /* liblibDeSmuME.a in Frameworks */,
|
||||
BFCADF1E25D22FE2008D78FB /* Systems.framework in Frameworks */,
|
||||
BF69FBC923E3A8380051BEEA /* libNestopia.a in Frameworks */,
|
||||
1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */,
|
||||
BFFDF03F23E3C28A00931B96 /* libGambatte.a in Frameworks */,
|
||||
BFFDF03723E3BB2600931B96 /* libSnes9x.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -394,7 +394,6 @@
|
||||
BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */,
|
||||
BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */,
|
||||
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */,
|
||||
BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */,
|
||||
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */,
|
||||
BFC6F7B71F435BC500221B96 /* Input+Display.swift */,
|
||||
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */,
|
||||
@ -403,6 +402,7 @@
|
||||
BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */,
|
||||
BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */,
|
||||
BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */,
|
||||
BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -420,7 +420,7 @@
|
||||
BF1DAD5B1D9F574900E752A7 /* Controller Skins */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */,
|
||||
BF1DAD5C1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift */,
|
||||
BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */,
|
||||
BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */,
|
||||
);
|
||||
@ -651,6 +651,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF4828851F9028F500028B97 /* System.swift */,
|
||||
BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */,
|
||||
);
|
||||
path = Systems;
|
||||
sourceTree = "<group>";
|
||||
@ -669,6 +670,17 @@
|
||||
BF9F4FCD1AAD7B25004C9500 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF6E70C225D22F7600E41CD1 /* Systems.framework */,
|
||||
BFDCA1E5244EBAA900B8FBDB /* liblibDeSmuME.a */,
|
||||
BF30AC25244E88BE00F0C744 /* libMelonDSDeltaCore.a */,
|
||||
BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */,
|
||||
BFFDF03D23E3C0F000931B96 /* libGambatte.a */,
|
||||
BFFDF03623E3BB2600931B96 /* libSnes9x.a */,
|
||||
BF69FBC823E3A8380051BEEA /* libNestopia.a */,
|
||||
BF69FBA923E399AA0051BEEA /* CoreMotion.framework */,
|
||||
BF69FBA723E3967B0051BEEA /* libz.tbd */,
|
||||
BF69FBA123E375A20051BEEA /* libVBA-M.a */,
|
||||
BF5ACE3823E23D6500BD0F20 /* libVBA-M.a */,
|
||||
BFDF71DA22F94CDF0074D92E /* DSDeltaCore.framework */,
|
||||
BFB359412278FD6700CFD920 /* N64DeltaCore_Video.framework */,
|
||||
BFB359422278FD6800CFD920 /* N64DeltaCore_RSP.framework */,
|
||||
@ -689,7 +701,7 @@
|
||||
BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */,
|
||||
BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */,
|
||||
22506DA00971C4300AF90A35 /* Pods.framework */,
|
||||
695E8C07A709B16AAD83AEC8 /* Pods_Delta.framework */,
|
||||
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -701,6 +713,7 @@
|
||||
BF5E7F451B9A652600AE44F8 /* Settings.storyboard */,
|
||||
BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */,
|
||||
BFB3645723245A6000CD0EB1 /* LicensesViewController.swift */,
|
||||
BFE9907E24451DE6006409A7 /* Cores */,
|
||||
BF71CF851FE8FFF1001F1613 /* App Icon Shortcuts */,
|
||||
BF11734E1DA32CEC00047DF8 /* Controllers */,
|
||||
BF1DAD5B1D9F574900E752A7 /* Controller Skins */,
|
||||
@ -741,6 +754,15 @@
|
||||
path = "Popover Menu";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFE9907E24451DE6006409A7 /* Cores */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFE9907F24451E15006409A7 /* MelonDSCoreSettingsViewController.swift */,
|
||||
BF00BEA525B758AA00C8607D /* SystemBIOS.swift */,
|
||||
);
|
||||
path = Cores;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFEC732F1AAECCBD00650035 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -754,8 +776,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF1020E21F95B05B00313182 /* DeltaToDelta2.xcmappingmodel */,
|
||||
BFEE943C23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel */,
|
||||
BF3D6C502202865F0083E05A /* Delta2ToDelta3.xcmappingmodel */,
|
||||
BF3D6C52220286750083E05A /* Delta3ToDelta4.xcmappingmodel */,
|
||||
BFDCA1E8244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel */,
|
||||
BFEF24F11F7DD4BE00454C62 /* Policies */,
|
||||
);
|
||||
path = Migrations;
|
||||
@ -873,15 +897,18 @@
|
||||
BFFA71D31AAC406100EE9DD1 /* Sources */,
|
||||
BFFA71D41AAC406100EE9DD1 /* Frameworks */,
|
||||
BFFA71D51AAC406100EE9DD1 /* Resources */,
|
||||
BF9F4FCC1AAD7AEE004C9500 /* Embed Frameworks */,
|
||||
B444B2BB31CBCEE7D86E943D /* [CP] Embed Pods Frameworks */,
|
||||
BF6BF3281EB897F6008E83CD /* Fabric */,
|
||||
0E0279E4F38215820BB0C9A0 /* [CP] Copy Pods Resources */,
|
||||
BF08DC3325CE07C3007A9CF4 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
BFEBE58325D3388F00222319 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Delta;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = Delta;
|
||||
productReference = BFFA71D71AAC406100EE9DD1 /* Delta.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@ -893,7 +920,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
DefaultBuildSystemTypeForWorkspace = Original;
|
||||
LastSwiftUpdateCheck = 0700;
|
||||
LastSwiftUpdateCheck = 1230;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = "Riley Testut";
|
||||
TargetAttributes = {
|
||||
@ -902,6 +929,11 @@
|
||||
DevelopmentTeam = 6XVY5G3U44;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
BF6E70B925D2187800E41CD1 = {
|
||||
CreatedOnToolsVersion = 12.5;
|
||||
DevelopmentTeam = 6XVY5G3U44;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
BFFA71D61AAC406100EE9DD1 = {
|
||||
CreatedOnToolsVersion = 6.3;
|
||||
DevelopmentTeam = 6XVY5G3U44;
|
||||
@ -931,6 +963,7 @@
|
||||
targets = (
|
||||
BFFA71D61AAC406100EE9DD1 /* Delta */,
|
||||
BF14D8941DE7A512002CA1BE /* mogenerator */,
|
||||
BF6E70B925D2187800E41CD1 /* Systems */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -957,26 +990,46 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
B444B2BB31CBCEE7D86E943D /* [CP] Embed Pods Frameworks */ = {
|
||||
0E0279E4F38215820BB0C9A0 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Delta/Pods-Delta-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SMCalloutView/SMCalloutView.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SQLite.swift/SQLite.framework",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Delta/Pods-Delta-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/DSDeltaCore/DSDeltaCore.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/DeltaCore/DeltaCore.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GBADeltaCore/GBADeltaCore.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/GBCDeltaCore/GBCDeltaCore.bundle",
|
||||
"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
|
||||
"${PODS_ROOT}/../External/Harmony/Harmony/Model/Core Data/Harmony.xcdatamodeld",
|
||||
"${PODS_ROOT}/../External/Harmony/Harmony/Model/Core Data/Migrations/HarmonyToHarmony2.xcmappingmodel",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/MelonDSDeltaCore/melonDS.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/N64DeltaCore/Mupen64Plus.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/NESDeltaCore/NESDeltaCore.bundle",
|
||||
"${PODS_ROOT}/../External/Roxas/Roxas/RSTCollectionViewCell.xib",
|
||||
"${PODS_ROOT}/../External/Roxas/Roxas/RSTPlaceholderView.xib",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SNESDeltaCore/SNESDeltaCore.bundle",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SMCalloutView.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLite.framework",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DSDeltaCore.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DeltaCore.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GBADeltaCore.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GBCDeltaCore.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Harmony.momd",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/HarmonyToHarmony2.cdm",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/melonDS.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Mupen64Plus.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/NESDeltaCore.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RSTCollectionViewCell.nib",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RSTPlaceholderView.nib",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SNESDeltaCore.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Delta/Pods-Delta-frameworks.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Delta/Pods-Delta-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
BF14D8981DE7A519002CA1BE /* mogenerator */ = {
|
||||
@ -1007,6 +1060,23 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Fabric/run\" d542629b4f6625cfd5564d27318550321272076d 333118df9345dcec21e4ba0bb7fa8f6c67c4eb41734374e24f6c71a8dcd5c870";
|
||||
};
|
||||
BF6E70BD25D2187F00E41CD1 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Manually build Systems.framework to prevent conflicts between Cocoapods' and SwiftPM's DeltaCore.\n\ncd $SRCROOT/Systems\n./build.sh\n";
|
||||
};
|
||||
DBD91E7D7EC2729786B4C5B1 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -1059,6 +1129,7 @@
|
||||
BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */,
|
||||
BF1020E31F95B05B00313182 /* DeltaToDelta2.xcmappingmodel in Sources */,
|
||||
BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */,
|
||||
BFEE943D23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel in Sources */,
|
||||
BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */,
|
||||
BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */,
|
||||
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */,
|
||||
@ -1080,7 +1151,7 @@
|
||||
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */,
|
||||
BFD1EF402336BD8800D197CF /* UIDevice+Processor.swift in Sources */,
|
||||
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */,
|
||||
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */,
|
||||
BF1DAD5D1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift in Sources */,
|
||||
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */,
|
||||
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */,
|
||||
BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */,
|
||||
@ -1104,9 +1175,11 @@
|
||||
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
|
||||
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */,
|
||||
BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */,
|
||||
BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */,
|
||||
BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */,
|
||||
BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */,
|
||||
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */,
|
||||
BFDCA1E9244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel in Sources */,
|
||||
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */,
|
||||
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */,
|
||||
BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */,
|
||||
@ -1125,6 +1198,7 @@
|
||||
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */,
|
||||
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */,
|
||||
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */,
|
||||
BFE9908024451E15006409A7 /* MelonDSCoreSettingsViewController.swift in Sources */,
|
||||
BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */,
|
||||
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,
|
||||
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */,
|
||||
@ -1134,6 +1208,7 @@
|
||||
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */,
|
||||
BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */,
|
||||
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */,
|
||||
BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */,
|
||||
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */,
|
||||
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */,
|
||||
BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */,
|
||||
@ -1143,6 +1218,14 @@
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
BFEBE58325D3388F00222319 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BF6E70B925D2187800E41CD1 /* Systems */;
|
||||
targetProxy = BFEBE58225D3388F00222319 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
BF353FF41C5D837600C1184C /* PauseMenu.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
@ -1199,6 +1282,24 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BF6E70BB25D2187800E41CD1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
BF6E70BC25D2187800E41CD1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BFFA71F41AAC406100EE9DD1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@ -1317,19 +1418,23 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_MODULES_AUTOLINK = NO;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.1;
|
||||
MARKETING_VERSION = 1.3;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta;
|
||||
PROVISIONING_PROFILE = "";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
STRIP_INSTALLED_PRODUCT = YES;
|
||||
STRIP_STYLE = "non-global";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = BETA;
|
||||
SWIFT_INCLUDE_PATHS = "$(inherited) \"$(SRCROOT)/Cores/GPGXDeltaCore/Sources/GPGXBridge\"";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -1342,18 +1447,22 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_MODULES_AUTOLINK = NO;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.1;
|
||||
MARKETING_VERSION = 1.3;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DIMPACTOR";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta;
|
||||
PROVISIONING_PROFILE = "";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
STRIP_INSTALLED_PRODUCT = YES;
|
||||
STRIP_STYLE = "non-global";
|
||||
SWIFT_INCLUDE_PATHS = "$(inherited) \"$(SRCROOT)/Cores/GPGXDeltaCore/Sources/GPGXBridge\"";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -1372,6 +1481,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BF6E70BA25D2187800E41CD1 /* Build configuration list for PBXAggregateTarget "Systems" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BF6E70BB25D2187800E41CD1 /* Debug */,
|
||||
BF6E70BC25D2187800E41CD1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BFFA71D21AAC406100EE9DD1 /* Build configuration list for PBXProject "Delta" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@ -1396,12 +1514,14 @@
|
||||
BF4828811F9027B600028B97 /* Delta.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
BFDCA1E7244F7DB100B8FBDB /* Delta 6.xcdatamodel */,
|
||||
BFE4275223EDF75300E6B417 /* Delta 5.xcdatamodel */,
|
||||
BF0758DE2202827C005110F2 /* Delta 4.xcdatamodel */,
|
||||
BF5645092202381000A8EA26 /* Delta 3.xcdatamodel */,
|
||||
BF4828821F9027B600028B97 /* Delta 2.xcdatamodel */,
|
||||
BF4828831F9027B600028B97 /* Delta.xcdatamodel */,
|
||||
);
|
||||
currentVersion = BF0758DE2202827C005110F2 /* Delta 4.xcdatamodel */;
|
||||
currentVersion = BFDCA1E7244F7DB100B8FBDB /* Delta 6.xcdatamodel */;
|
||||
path = Delta.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
||||
@ -14,120 +14,8 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFADAFF719AE7BB70050CF31"
|
||||
BuildableName = "Roxas.framework"
|
||||
BlueprintName = "Roxas"
|
||||
ReferencedContainer = "container:External/Roxas/Roxas.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFA1C8D31ECD01C100DEA99D"
|
||||
BuildableName = "Harmony.framework"
|
||||
BlueprintName = "Harmony"
|
||||
ReferencedContainer = "container:External/Harmony/Harmony.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFECF9F82016982D0012B9FC"
|
||||
BuildableName = "Harmony_Drive.framework"
|
||||
BlueprintName = "Harmony-Drive"
|
||||
ReferencedContainer = "container:External/Harmony/Backends/Drive/Harmony-Drive.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF46895C1AACF36800A2586D"
|
||||
BuildableName = "DeltaCore.framework"
|
||||
BlueprintName = "DeltaCore"
|
||||
ReferencedContainer = "container:Cores/DeltaCore/DeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF3C12F220438F3F0079A4B5"
|
||||
BuildableName = "NESDeltaCore.framework"
|
||||
BlueprintName = "NESDeltaCore"
|
||||
ReferencedContainer = "container:Cores/NESDeltaCore/NESDeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF9F4FDB1AAD8070004C9500"
|
||||
BuildableName = "SNESDeltaCore.framework"
|
||||
BlueprintName = "SNESDeltaCore"
|
||||
ReferencedContainer = "container:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF8F2AAD1E9C879300F89F15"
|
||||
BuildableName = "GBCDeltaCore.framework"
|
||||
BlueprintName = "GBCDeltaCore"
|
||||
ReferencedContainer = "container:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFE8E9C91D010AF7009D623D"
|
||||
BuildableName = "GBADeltaCore.framework"
|
||||
BlueprintName = "GBADeltaCore"
|
||||
ReferencedContainer = "container:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1AC0A77D7D4F472C1693D90C57B90DD5"
|
||||
BuildableName = "Pods_Delta.framework"
|
||||
BlueprintIdentifier = "33C94426DAF58519DC6806AF4C44C9E7"
|
||||
BuildableName = "libPods-Delta.a"
|
||||
BlueprintName = "Pods-Delta"
|
||||
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
@ -153,8 +41,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@ -164,13 +50,14 @@
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@ -208,8 +95,6 @@
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
67
Delta.xcodeproj/xcshareddata/xcschemes/Systems.xcscheme
Normal file
67
Delta.xcodeproj/xcshareddata/xcschemes/Systems.xcscheme
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
|
||||
BuildableName = "Systems"
|
||||
BlueprintName = "Systems"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
|
||||
BuildableName = "Systems"
|
||||
BlueprintName = "Systems"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
6
Delta.xcworkspace/contents.xcworkspacedata
generated
6
Delta.xcworkspace/contents.xcworkspacedata
generated
@ -22,9 +22,15 @@
|
||||
<FileRef
|
||||
location = "group:Cores/N64DeltaCore/N64DeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/MelonDSDeltaCore/MelonDSDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/DSDeltaCore/DSDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/GPGXDeltaCore">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/Harmony/Harmony.xcodeproj">
|
||||
</FileRef>
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Original</string>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
25
Delta.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
25
Delta.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "DeltaCore",
|
||||
"repositoryURL": "https://github.com/rileytestut/DeltaCore.git",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "eeefb30bd78fb130f1113e823afbce6f4f767cfb",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ZIPFoundation",
|
||||
"repositoryURL": "https://github.com/weichsel/ZIPFoundation.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "ec32d62d412578542c0ffb7a6ce34d3e64b43b94",
|
||||
"version": "0.9.11"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
import Harmony_Dropbox
|
||||
import Harmony
|
||||
|
||||
import Fabric
|
||||
import Crashlytics
|
||||
@ -128,7 +128,7 @@ private extension AppDelegate
|
||||
#if BETA
|
||||
System.allCases.forEach { Delta.register($0.deltaCore) }
|
||||
#else
|
||||
System.allCases.filter { $0 != .ds }.forEach { Delta.register($0.deltaCore) }
|
||||
System.allCases.filter { $0 != .genesis }.forEach { Delta.register($0.deltaCore) }
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -25,7 +23,7 @@
|
||||
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
|
||||
<connections>
|
||||
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/>
|
||||
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindToGameCollectionViewController:" id="nzI-4n-kDg"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
@ -41,7 +39,7 @@
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="6bq-zy-UZU" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="uzY-vR-coL">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -83,6 +83,8 @@
|
||||
<connections>
|
||||
<segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/>
|
||||
<segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="1Xp-2J-0cq"/>
|
||||
<segue destination="qdE-gb-V2e" kind="presentation" identifier="preferredControllerSkins" id="i6y-cP-3WM"/>
|
||||
<segue destination="V2x-v0-jWm" kind="presentation" identifier="showDSSettings" id="kuV-tY-Y0B"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@ -217,7 +219,7 @@
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/>
|
||||
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindFromSaveStatesViewControllerWith:" id="dwO-iv-XDr"/>
|
||||
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindToGameCollectionViewController:" id="dwO-iv-XDr"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@ -225,6 +227,16 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2652" y="1718"/>
|
||||
</scene>
|
||||
<!--preferredControllerSkins-->
|
||||
<scene sceneID="aKY-Ld-et6">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Settings" referencedIdentifier="preferredControllerSkins" id="dbc-pQ-iun" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="xth-MV-SHp"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="za6-AO-ZFe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3405" y="2394"/>
|
||||
</scene>
|
||||
<!--saveStatesViewController-->
|
||||
<scene sceneID="f1R-Kb-FOU">
|
||||
<objects>
|
||||
@ -235,11 +247,56 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3409" y="1716"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="eMh-8N-ZGA">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qdE-gb-V2e" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationItem key="navigationItem" id="Dg6-He-v5H"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="35T-4Q-Mmp">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="dbc-pQ-iun" kind="relationship" relationship="rootViewController" id="oRb-B6-c0J"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="XmB-QY-yA3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2652" y="2394"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="OW2-zT-pbF">
|
||||
<objects>
|
||||
<navigationController id="V2x-v0-jWm" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="pjb-4I-yar">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="cFV-KV-B18" kind="relationship" relationship="rootViewController" id="VBP-fg-oNH"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Jo9-gl-p5p" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2652" y="3085"/>
|
||||
</scene>
|
||||
<!--dsSettingsViewController-->
|
||||
<scene sceneID="anM-Cb-BaB">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Settings" referencedIdentifier="dsSettingsViewController" id="cFV-KV-B18" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="Dkm-Hm-sQa"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="IS2-hO-HBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3258" y="3084"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="SettingsButton" width="22" height="22"/>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="Tey-6Z-UHp"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="SettingsButton" width="22" height="22"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -167,7 +168,7 @@
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" id="has-I3-HDZ">
|
||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="y2a-9f-EFz">
|
||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="y2a-9f-EFz">
|
||||
<rect key="frame" x="288.5" y="13" width="30" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="▼"/>
|
||||
@ -206,7 +207,7 @@
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="45"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" id="9bA-Tg-Bko">
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="9bA-Tg-Bko">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="45"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
@ -266,7 +267,7 @@
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Name" id="QT6-DZ-g70">
|
||||
<cells>
|
||||
@ -337,7 +338,7 @@
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="210"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>
|
||||
<connections>
|
||||
@ -398,4 +399,9 @@
|
||||
<point key="canvasLocation" x="2385" y="1377"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="labelColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,8 +17,7 @@ extension Action
|
||||
case destructive
|
||||
case selected
|
||||
|
||||
var alertActionStyle: UIAlertAction.Style
|
||||
{
|
||||
var alertActionStyle: UIAlertAction.Style {
|
||||
switch self
|
||||
{
|
||||
case .default, .selected: return .default
|
||||
@ -27,8 +26,7 @@ extension Action
|
||||
}
|
||||
}
|
||||
|
||||
var previewActionStyle: UIPreviewAction.Style?
|
||||
{
|
||||
var previewActionStyle: UIPreviewAction.Style? {
|
||||
switch self
|
||||
{
|
||||
case .default: return .default
|
||||
@ -40,11 +38,40 @@ extension Action
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension Action.Style
|
||||
{
|
||||
var menuAttributes: UIMenuElement.Attributes {
|
||||
switch self
|
||||
{
|
||||
case .default, .cancel, .selected: return []
|
||||
case .destructive: return .destructive
|
||||
}
|
||||
}
|
||||
|
||||
var menuState: UIMenuElement.State {
|
||||
switch self
|
||||
{
|
||||
case .default, .cancel, .destructive: return .off
|
||||
case .selected: return .on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Action
|
||||
{
|
||||
let title: String
|
||||
let style: Style
|
||||
let action: ((Action) -> Void)?
|
||||
var title: String
|
||||
var style: Style
|
||||
var image: UIImage? = nil
|
||||
var action: ((Action) -> Void)?
|
||||
|
||||
init(title: String, style: Style = .default, image: UIImage? = nil, action: ((Action) -> Void)? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.style = style
|
||||
self.image = image
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
extension UIAlertAction
|
||||
@ -82,6 +109,19 @@ extension UIAlertController
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension UIAction
|
||||
{
|
||||
convenience init?(_ action: Action)
|
||||
{
|
||||
guard action.style != .cancel else { return nil }
|
||||
|
||||
self.init(title: action.title, image: action.image, attributes: action.style.menuAttributes, state: action.style.menuState) { _ in
|
||||
action.action?(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RangeReplaceableCollection where Iterator.Element == Action
|
||||
{
|
||||
var alertActions: [UIAlertAction] {
|
||||
@ -93,4 +133,10 @@ extension RangeReplaceableCollection where Iterator.Element == Action
|
||||
let actions = self.compactMap { UIPreviewAction($0) }
|
||||
return actions
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
var menuActions: [UIAction] {
|
||||
let actions = self.compactMap { UIAction($0) }
|
||||
return actions
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,8 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
|
||||
return interitemSpacing
|
||||
}
|
||||
|
||||
private var cachedLayoutAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
|
||||
|
||||
override var estimatedItemSize: CGSize {
|
||||
didSet {
|
||||
fatalError("GridCollectionViewLayout does not support self-sizing cells.")
|
||||
@ -137,9 +139,24 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
|
||||
}
|
||||
}
|
||||
|
||||
for attributes in layoutAttributes
|
||||
{
|
||||
// Update cached attributes for layoutAttributesForItem(at:)
|
||||
self.cachedLayoutAttributes[attributes.indexPath] = attributes
|
||||
}
|
||||
|
||||
return layoutAttributes
|
||||
}
|
||||
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
|
||||
{
|
||||
if let cachedAttributes = self.cachedLayoutAttributes[indexPath]
|
||||
{
|
||||
return cachedAttributes
|
||||
}
|
||||
|
||||
return super.layoutAttributesForItem(at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private extension GridCollectionViewLayout
|
||||
|
||||
@ -63,6 +63,8 @@ class PopoverMenuButton: UIControl
|
||||
private let arrowLabel: UILabel
|
||||
private let stackView: UIStackView
|
||||
|
||||
private var _didLayoutSubviews = false
|
||||
|
||||
private var parentNavigationBar: UINavigationBar? {
|
||||
guard let navigationController = self.parentViewController as? UINavigationController ?? self.parentViewController?.navigationController else { return nil }
|
||||
guard self.isDescendant(of: navigationController.navigationBar) else { return nil }
|
||||
@ -104,6 +106,21 @@ class PopoverMenuButton: UIControl
|
||||
{
|
||||
self.updateTextAttributes()
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
if !_didLayoutSubviews
|
||||
{
|
||||
_didLayoutSubviews = true
|
||||
|
||||
// didMoveToSuperview() can be too early to accurately
|
||||
// update text attributes, so ensure we also update
|
||||
// during first layoutSubviews() call.
|
||||
self.updateTextAttributes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PopoverMenuButton
|
||||
|
||||
@ -14,6 +14,7 @@ import DeltaCore
|
||||
import Harmony
|
||||
import Roxas
|
||||
import ZIPFoundation
|
||||
import MelonDSDeltaCore
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
@ -22,13 +23,24 @@ extension DatabaseManager
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
enum ImportError: Error, Hashable, Equatable
|
||||
enum ImportError: LocalizedError, Hashable, Equatable
|
||||
{
|
||||
case doesNotExist(URL)
|
||||
case invalid(URL)
|
||||
case unsupported(URL)
|
||||
case unknown(URL, NSError)
|
||||
case saveFailed(Set<URL>, NSError)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .doesNotExist: return NSLocalizedString("The file does not exist.", comment: "")
|
||||
case .invalid: return NSLocalizedString("The file is invalid.", comment: "")
|
||||
case .unsupported: return NSLocalizedString("This file is not supported.", comment: "")
|
||||
case .unknown(_, let error): return error.localizedDescription
|
||||
case .saveFailed(_, let error): return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +54,8 @@ final class DatabaseManager: RSTPersistentContainer
|
||||
|
||||
private var validationManagedObjectContext: NSManagedObjectContext?
|
||||
|
||||
private let importController = ImportController(documentTypes: [])
|
||||
|
||||
private init()
|
||||
{
|
||||
guard
|
||||
@ -62,6 +76,12 @@ extension DatabaseManager
|
||||
{
|
||||
guard !self.isStarted else { return }
|
||||
|
||||
for description in self.persistentStoreDescriptions
|
||||
{
|
||||
// Set configuration so RSTPersistentContainer can determine how to migrate this and Harmony's database independently.
|
||||
description.configuration = NSManagedObjectModel.Configuration.external.rawValue
|
||||
}
|
||||
|
||||
self.loadPersistentStores { (description, error) in
|
||||
guard error == nil else { return completionHandler(error) }
|
||||
|
||||
@ -74,6 +94,114 @@ extension DatabaseManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(_ core: DeltaCoreProtocol, in context: NSManagedObjectContext)
|
||||
{
|
||||
guard let system = System(gameType: core.gameType) else { return }
|
||||
|
||||
if let skin = ControllerSkin(system: system, context: context)
|
||||
{
|
||||
print("Updated default skin (\(skin.identifier)) for system:", system)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Failed to update default skin for system:", system)
|
||||
}
|
||||
|
||||
switch system
|
||||
{
|
||||
case .ds where core == MelonDS.core:
|
||||
|
||||
// Returns nil if game already exists.
|
||||
func makeBIOS(name: String, identifier: String) -> Game?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), identifier)
|
||||
if let _ = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self).first
|
||||
{
|
||||
// BIOS already exists, so don't do anything.
|
||||
return nil
|
||||
}
|
||||
|
||||
let filename: String
|
||||
|
||||
switch identifier
|
||||
{
|
||||
case Game.melonDSBIOSIdentifier:
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios7URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios9URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.firmwareURL.path)
|
||||
else { return nil }
|
||||
|
||||
filename = "nds.bios"
|
||||
|
||||
case Game.melonDSDSiBIOSIdentifier:
|
||||
#if BETA
|
||||
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS7URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS9URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiFirmwareURL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiNANDURL.path)
|
||||
else { return nil }
|
||||
|
||||
filename = "dsi.bios"
|
||||
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
|
||||
default: filename = "system.bios"
|
||||
}
|
||||
|
||||
let bios = Game(context: context)
|
||||
bios.name = name
|
||||
bios.identifier = identifier
|
||||
bios.type = .ds
|
||||
bios.filename = filename
|
||||
|
||||
if let artwork = UIImage(named: "DS Home Screen"), let artworkData = artwork.pngData()
|
||||
{
|
||||
do
|
||||
{
|
||||
let destinationURL = DatabaseManager.artworkURL(for: bios)
|
||||
try artworkData.write(to: destinationURL, options: .atomic)
|
||||
bios.artworkURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to copy default DS home screen artwork.", error)
|
||||
}
|
||||
}
|
||||
|
||||
return bios
|
||||
}
|
||||
|
||||
let insertedGames = [
|
||||
(name: NSLocalizedString("Home Screen", comment: ""), identifier: Game.melonDSBIOSIdentifier),
|
||||
(name: NSLocalizedString("Home Screen (DSi)", comment: ""), identifier: Game.melonDSDSiBIOSIdentifier)
|
||||
].compactMap(makeBIOS)
|
||||
|
||||
// Break if we didn't create any new Games.
|
||||
guard !insertedGames.isEmpty else { break }
|
||||
|
||||
let gameCollection = GameCollection(context: context)
|
||||
gameCollection.identifier = GameType.ds.rawValue
|
||||
gameCollection.index = Int16(System.ds.year)
|
||||
gameCollection.games.formUnion(insertedGames)
|
||||
|
||||
case .ds:
|
||||
let predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), [Game.melonDSBIOSIdentifier, Game.melonDSDSiBIOSIdentifier])
|
||||
|
||||
let games = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self)
|
||||
for game in games
|
||||
{
|
||||
context.delete(game)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Update -
|
||||
@ -113,13 +241,7 @@ private extension DatabaseManager
|
||||
|
||||
for system in System.allCases
|
||||
{
|
||||
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { continue }
|
||||
|
||||
let controllerSkin = ControllerSkin(context: context)
|
||||
controllerSkin.isStandard = true
|
||||
controllerSkin.filename = deltaControllerSkin.fileURL.lastPathComponent
|
||||
|
||||
controllerSkin.configure(with: deltaControllerSkin)
|
||||
self.prepare(system.deltaCore, in: context)
|
||||
}
|
||||
|
||||
do
|
||||
@ -157,7 +279,20 @@ extension DatabaseManager
|
||||
{
|
||||
func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?)
|
||||
{
|
||||
var errors = Set<ImportError>()
|
||||
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
|
||||
guard externalFileURLs.isEmpty else {
|
||||
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
|
||||
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
|
||||
availableFileURLs.formUnion(importedURLs)
|
||||
|
||||
self.importGames(at: Set(availableFileURLs)) { (importedGames, importErrors) in
|
||||
let allErrors = importErrors.union(externalImportErrors)
|
||||
completion?(importedGames, allErrors)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
|
||||
if zipFileURLs.count > 0
|
||||
@ -175,6 +310,7 @@ extension DatabaseManager
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
var errors = Set<ImportError>()
|
||||
var identifiers = Set<String>()
|
||||
|
||||
for url in urls
|
||||
@ -270,10 +406,24 @@ extension DatabaseManager
|
||||
|
||||
func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?)
|
||||
{
|
||||
var errors = Set<ImportError>()
|
||||
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
|
||||
guard externalFileURLs.isEmpty else {
|
||||
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
|
||||
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
|
||||
availableFileURLs.formUnion(importedURLs)
|
||||
|
||||
self.importControllerSkins(at: Set(availableFileURLs)) { (importedSkins, importErrors) in
|
||||
let allErrors = importErrors.union(externalImportErrors)
|
||||
completion?(importedSkins, allErrors)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
var errors = Set<ImportError>()
|
||||
var identifiers = Set<String>()
|
||||
|
||||
for url in urls
|
||||
@ -412,6 +562,32 @@ extension DatabaseManager
|
||||
completion(outputURLs, errors)
|
||||
}
|
||||
}
|
||||
|
||||
private func importExternalFiles(at urls: Set<URL>, completion: @escaping ((Set<URL>, Set<ImportError>) -> Void))
|
||||
{
|
||||
var outputURLs = Set<URL>()
|
||||
var errors = Set<ImportError>()
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for url in urls
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.importController.importExternalFile(at: url) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): errors.insert(.unknown(url, error as NSError))
|
||||
case .success(let fileURL): outputURLs.insert(fileURL)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
completion(outputURLs, errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - File URLs -
|
||||
@ -487,7 +663,7 @@ extension DatabaseManager
|
||||
{
|
||||
let gameURL = game.fileURL
|
||||
|
||||
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("jpg")
|
||||
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("png")
|
||||
return artworkURL
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Delta 4.xcdatamodel</string>
|
||||
<string>Delta 6.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="163"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
|
||||
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="coreIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="163"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
|
||||
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="178"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -86,9 +86,9 @@ extension ControllerSkin: ControllerSkinProtocol
|
||||
return self.controllerSkin?.isTranslucent(for: traits)
|
||||
}
|
||||
|
||||
public func gameScreenFrame(for traits: DeltaCore.ControllerSkin.Traits) -> CGRect?
|
||||
public func screens(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Screen]?
|
||||
{
|
||||
return self.controllerSkin?.gameScreenFrame(for: traits)
|
||||
return self.controllerSkin?.screens(for: traits)
|
||||
}
|
||||
|
||||
public func aspectRatio(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
|
||||
|
||||
@ -9,8 +9,16 @@
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import MelonDSDeltaCore
|
||||
|
||||
import Harmony
|
||||
|
||||
public extension Game
|
||||
{
|
||||
static let melonDSBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.BIOS"
|
||||
static let melonDSDSiBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.DSiBIOS"
|
||||
}
|
||||
|
||||
@objc(Game)
|
||||
public class Game: _Game, GameProtocol
|
||||
{
|
||||
@ -30,10 +38,24 @@ public class Game: _Game, GameProtocol
|
||||
var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL
|
||||
self.didAccessValue(forKey: #keyPath(Game.artworkURL))
|
||||
|
||||
if let unwrappedArtworkURL = artworkURL, unwrappedArtworkURL.isFileURL
|
||||
if let unwrappedArtworkURL = artworkURL
|
||||
{
|
||||
// Recreate the stored URL relative to current sandbox location.
|
||||
artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL)
|
||||
if unwrappedArtworkURL.isFileURL
|
||||
{
|
||||
// Recreate the stored URL relative to current sandbox location.
|
||||
artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL)
|
||||
}
|
||||
else if unwrappedArtworkURL.host?.lowercased() == "img.gamefaqs.net", var components = URLComponents(url: unwrappedArtworkURL, resolvingAgainstBaseURL: false)
|
||||
{
|
||||
// Quick fix for broken album artwork URLs due to host change.
|
||||
components.host = "gamefaqs1.cbsistatic.com"
|
||||
components.scheme = "https"
|
||||
|
||||
if let url = components.url
|
||||
{
|
||||
artworkURL = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artworkURL
|
||||
@ -122,12 +144,42 @@ extension Game: Syncable
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
let gameFile = File(identifier: "game", fileURL: self.fileURL)
|
||||
let artworkURL: URL
|
||||
|
||||
if let fileURL = self.artworkURL, fileURL.isFileURL
|
||||
{
|
||||
artworkURL = fileURL
|
||||
}
|
||||
else
|
||||
{
|
||||
artworkURL = DatabaseManager.artworkURL(for: self)
|
||||
}
|
||||
|
||||
let artworkURL = DatabaseManager.artworkURL(for: self)
|
||||
let artworkFile = File(identifier: "artwork", fileURL: artworkURL)
|
||||
|
||||
return [gameFile, artworkFile]
|
||||
|
||||
switch self.identifier
|
||||
{
|
||||
case Game.melonDSBIOSIdentifier:
|
||||
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.bios7URL)
|
||||
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.bios9URL)
|
||||
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.firmwareURL)
|
||||
|
||||
return [artworkFile, bios7File, bios9File, firmwareFile]
|
||||
|
||||
case Game.melonDSDSiBIOSIdentifier:
|
||||
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS7URL)
|
||||
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS9URL)
|
||||
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.dsiFirmwareURL)
|
||||
|
||||
// DSi NAND is ~240MB, so don't sync for now until Harmony can selectively download files.
|
||||
// let nandFile = File(identifier: "nand", fileURL: MelonDSEmulatorBridge.shared.dsiNANDURL)
|
||||
|
||||
return [artworkFile, bios7File, bios9File, firmwareFile]
|
||||
|
||||
default:
|
||||
let gameFile = File(identifier: "game", fileURL: self.fileURL)
|
||||
return [artworkFile, gameFile]
|
||||
}
|
||||
}
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import GBCDeltaCore
|
||||
|
||||
import Harmony
|
||||
|
||||
@objc(GameSave)
|
||||
@ -57,4 +59,11 @@ extension GameSave: Syncable
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.game?.name
|
||||
}
|
||||
|
||||
public var isSyncingEnabled: Bool {
|
||||
// self.game may be nil if being downloaded, so don't enforce it.
|
||||
// guard let identifier = self.game?.identifier else { return false }
|
||||
|
||||
return self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ import Foundation
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
@objc public enum SaveStateType: Int16
|
||||
{
|
||||
case auto
|
||||
@ -111,7 +113,7 @@ extension SaveState: Syncable
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\SaveState.creationDate, \SaveState.filename, \SaveState.modifiedDate, \SaveState.name, \SaveState.type]
|
||||
return [\SaveState.creationDate, \SaveState.filename, \SaveState.modifiedDate, \SaveState.name, \SaveState.type, \SaveState.coreIdentifier]
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
@ -123,15 +125,40 @@ extension SaveState: Syncable
|
||||
}
|
||||
|
||||
public var isSyncingEnabled: Bool {
|
||||
return self.type != .auto && self.type != .quick
|
||||
// self.game may be nil if being downloaded, so don't enforce it.
|
||||
// guard let identifier = self.game?.identifier else { return false }
|
||||
|
||||
let isSyncingEnabled = (self.type != .auto && self.type != .quick) && (self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier)
|
||||
return isSyncingEnabled
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
return [.gameID: game.identifier, .gameName: game.name]
|
||||
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier].compactMapValues { $0 }
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.localizedName
|
||||
}
|
||||
|
||||
public func awakeFromSync(_ record: AnyRecord)
|
||||
{
|
||||
guard self.coreIdentifier == nil else { return }
|
||||
guard let game = self.game, let system = System(gameType: game.type) else { return }
|
||||
|
||||
if let coreIdentifier = record.remoteMetadata?[.coreID]
|
||||
{
|
||||
// SaveState was synced to older version of Delta and lost its coreIdentifier,
|
||||
// but it remains in the remote metadata so we can reassign it.
|
||||
self.coreIdentifier = coreIdentifier
|
||||
}
|
||||
else
|
||||
{
|
||||
switch system
|
||||
{
|
||||
case .ds: self.coreIdentifier = DS.core.identifier // Assume DS save state with nil coreIdentifier is from DeSmuME core.
|
||||
default: self.coreIdentifier = system.deltaCore.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,5 +28,9 @@ public class _ControllerSkin: NSManagedObject
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
@NSManaged public var preferredLandscapeSkinByGames: Set<Game>
|
||||
|
||||
@NSManaged public var preferredPortraitSkinByGames: Set<Game>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,10 @@ public class _Game: NSManagedObject
|
||||
|
||||
@NSManaged public var gameSave: GameSave?
|
||||
|
||||
@NSManaged public var preferredLandscapeSkin: ControllerSkin?
|
||||
|
||||
@NSManaged public var preferredPortraitSkin: ControllerSkin?
|
||||
|
||||
@NSManaged public var previewSaveState: SaveState?
|
||||
|
||||
@NSManaged public var saveStates: Set<SaveState>
|
||||
|
||||
@ -14,6 +14,8 @@ public class _SaveState: NSManagedObject
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@NSManaged public var coreIdentifier: String?
|
||||
|
||||
@NSManaged public var creationDate: Date
|
||||
|
||||
@NSManaged public var filename: String
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -8,6 +8,10 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
@objc(SaveStateToSaveStateMigrationPolicy)
|
||||
class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy
|
||||
{
|
||||
@ -23,3 +27,19 @@ class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delta5 to Delta6
|
||||
extension SaveStateToSaveStateMigrationPolicy
|
||||
{
|
||||
@objc(defaultCoreIdentifierForGameType:)
|
||||
func defaultCoreIdentifier(for gameType: GameType) -> String?
|
||||
{
|
||||
guard let system = System(gameType: gameType) else { return nil }
|
||||
|
||||
switch system
|
||||
{
|
||||
case .ds: return DS.core.identifier // Assume any existing save state is from DeSmuME.
|
||||
default: return system.deltaCore.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ enum ActionInput: String
|
||||
case quickSave
|
||||
case quickLoad
|
||||
case fastForward
|
||||
case toggleFastForward
|
||||
}
|
||||
|
||||
extension ActionInput: Input
|
||||
|
||||
@ -10,11 +10,23 @@ import UIKit
|
||||
|
||||
import DeltaCore
|
||||
import GBADeltaCore
|
||||
import Systems
|
||||
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
import Roxas
|
||||
|
||||
private var kvoContext = 0
|
||||
|
||||
private extension DeltaCore.ControllerSkin
|
||||
{
|
||||
func hasTouchScreen(for traits: DeltaCore.ControllerSkin.Traits) -> Bool
|
||||
{
|
||||
let hasTouchScreen = self.items(for: traits)?.contains(where: { $0.kind == .touchScreen }) ?? false
|
||||
return hasTouchScreen
|
||||
}
|
||||
}
|
||||
|
||||
private extension GameViewController
|
||||
{
|
||||
struct PausedSaveState: SaveStateProtocol
|
||||
@ -31,6 +43,29 @@ private extension GameViewController
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultInputMapping: GameControllerInputMappingProtocol
|
||||
{
|
||||
let gameController: GameController
|
||||
|
||||
var gameControllerInputType: GameControllerInputType {
|
||||
return self.gameController.inputType
|
||||
}
|
||||
|
||||
func input(forControllerInput controllerInput: Input) -> Input?
|
||||
{
|
||||
if let mappedInput = self.gameController.defaultInputMapping?.input(forControllerInput: controllerInput)
|
||||
{
|
||||
return mappedInput
|
||||
}
|
||||
|
||||
// Only intercept controller skin inputs.
|
||||
guard controllerInput.type == .controller(.controllerSkin) else { return nil }
|
||||
|
||||
let actionInput = ActionInput(stringValue: controllerInput.stringValue)
|
||||
return actionInput
|
||||
}
|
||||
}
|
||||
|
||||
struct SustainInputsMapping: GameControllerInputMappingProtocol
|
||||
{
|
||||
let gameController: GameController
|
||||
@ -74,7 +109,6 @@ class GameViewController: DeltaCore.GameViewController
|
||||
self.shouldResetSustainedInputs = true
|
||||
}
|
||||
|
||||
self.updateControllerSkin()
|
||||
self.updateControllers()
|
||||
|
||||
self.presentedGyroAlert = false
|
||||
@ -169,6 +203,8 @@ class GameViewController: DeltaCore.GameViewController
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didActivateGyro(with:)), name: GBA.didActivateGyroNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didDeactivateGyro(with:)), name: GBA.didDeactivateGyroNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.emulationDidQuit(with:)), name: EmulatorCore.emulationDidQuitNotification, object: nil)
|
||||
}
|
||||
|
||||
deinit
|
||||
@ -190,7 +226,7 @@ class GameViewController: DeltaCore.GameViewController
|
||||
self.inputsToSustain[AnyInput(input)] = value
|
||||
}
|
||||
}
|
||||
else if self.emulatorCore?.state == .running
|
||||
else if let emulatorCore = self.emulatorCore, emulatorCore.state == .running
|
||||
{
|
||||
guard let actionInput = ActionInput(input: input) else { return }
|
||||
|
||||
@ -199,6 +235,9 @@ class GameViewController: DeltaCore.GameViewController
|
||||
case .quickSave: self.performQuickSaveAction()
|
||||
case .quickLoad: self.performQuickLoadAction()
|
||||
case .fastForward: self.performFastForwardAction(activate: true)
|
||||
case .toggleFastForward:
|
||||
let isFastForwarding = (emulatorCore.rate != emulatorCore.deltaCore.supportedRates.lowerBound)
|
||||
self.performFastForwardAction(activate: !isFastForwarding)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -223,6 +262,7 @@ class GameViewController: DeltaCore.GameViewController
|
||||
case .quickSave: break
|
||||
case .quickLoad: break
|
||||
case .fastForward: self.performFastForwardAction(activate: false)
|
||||
case .toggleFastForward: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,14 +280,12 @@ extension GameViewController
|
||||
// Lays out self.gameView, so we can pin self.sustainButtonsContentView to it without resulting in a temporary "cannot satisfy constraints".
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
let gameViewContainerView = self.gameView.superview!
|
||||
|
||||
self.controllerView.translucentControllerSkinOpacity = Settings.translucentControllerSkinOpacity
|
||||
|
||||
self.sustainButtonsContentView = UIView(frame: CGRect(x: 0, y: 0, width: self.gameView.bounds.width, height: self.gameView.bounds.height))
|
||||
self.sustainButtonsContentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.sustainButtonsContentView.isHidden = true
|
||||
self.view.insertSubview(self.sustainButtonsContentView, aboveSubview: gameViewContainerView)
|
||||
self.view.insertSubview(self.sustainButtonsContentView, aboveSubview: self.gameView)
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .dark)
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
||||
@ -273,15 +311,27 @@ extension GameViewController
|
||||
vibrancyView.contentView.addSubview(self.sustainButtonsBackgroundView)
|
||||
|
||||
// Auto Layout
|
||||
self.sustainButtonsContentView.leadingAnchor.constraint(equalTo: gameViewContainerView.leadingAnchor).isActive = true
|
||||
self.sustainButtonsContentView.trailingAnchor.constraint(equalTo: gameViewContainerView.trailingAnchor).isActive = true
|
||||
self.sustainButtonsContentView.topAnchor.constraint(equalTo: gameViewContainerView.topAnchor).isActive = true
|
||||
self.sustainButtonsContentView.bottomAnchor.constraint(equalTo: gameViewContainerView.bottomAnchor).isActive = true
|
||||
self.sustainButtonsContentView.leadingAnchor.constraint(equalTo: self.gameView.leadingAnchor).isActive = true
|
||||
self.sustainButtonsContentView.trailingAnchor.constraint(equalTo: self.gameView.trailingAnchor).isActive = true
|
||||
self.sustainButtonsContentView.topAnchor.constraint(equalTo: self.gameView.topAnchor).isActive = true
|
||||
self.sustainButtonsContentView.bottomAnchor.constraint(equalTo: self.gameView.bottomAnchor).isActive = true
|
||||
|
||||
self.updateControllerSkin()
|
||||
self.updateControllers()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if self.emulatorCore?.deltaCore == DS.core, UserDefaults.standard.desmumeDeprecatedAlertCount < 3
|
||||
{
|
||||
let toastView = RSTToastView(text: NSLocalizedString("DeSmuME Core Deprecated", comment: ""), detailText: NSLocalizedString("Switch to the melonDS core in Settings for latest improvements.", comment: ""))
|
||||
self.show(toastView, duration: 5.0)
|
||||
|
||||
UserDefaults.standard.desmumeDeprecatedAlertCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
|
||||
{
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
@ -302,10 +352,18 @@ extension GameViewController
|
||||
{
|
||||
case "showGamesViewController":
|
||||
let gamesViewController = (segue.destination as! UINavigationController).topViewController as! GamesViewController
|
||||
gamesViewController.theme = .translucent
|
||||
gamesViewController.activeEmulatorCore = self.emulatorCore
|
||||
|
||||
self.updateAutoSaveState()
|
||||
if let emulatorCore = self.emulatorCore
|
||||
{
|
||||
gamesViewController.theme = .translucent
|
||||
gamesViewController.activeEmulatorCore = emulatorCore
|
||||
|
||||
self.updateAutoSaveState()
|
||||
}
|
||||
else
|
||||
{
|
||||
gamesViewController.theme = .opaque
|
||||
}
|
||||
|
||||
case "pause":
|
||||
|
||||
@ -352,17 +410,21 @@ extension GameViewController
|
||||
self.pausingGameController = gameController
|
||||
}
|
||||
|
||||
if self.emulatorCore?.deltaCore.supportedRates.upperBound == 1
|
||||
{
|
||||
pauseViewController.fastForwardItem = nil
|
||||
}
|
||||
|
||||
switch self.game?.type
|
||||
{
|
||||
case .n64? where !UIDevice.current.hasA9ProcessorOrBetter:
|
||||
// A8 processors and earlier aren't powerful enough to run N64 games faster than 1x speed.
|
||||
pauseViewController.fastForwardItem = nil
|
||||
|
||||
case .ds?:
|
||||
// Cheats and Fast Forwarding are not yet supported for DS games.
|
||||
case .ds? where self.emulatorCore?.deltaCore == DS.core:
|
||||
// Cheats are not supported by DeSmuME core.
|
||||
pauseViewController.cheatCodesItem = nil
|
||||
pauseViewController.fastForwardItem = nil
|
||||
|
||||
case .genesis?:
|
||||
// GPGX core does not support cheats yet.
|
||||
pauseViewController.cheatCodesItem = nil
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
@ -491,16 +553,27 @@ private extension GameViewController
|
||||
}
|
||||
|
||||
// If Settings.localControllerPlayerIndex is non-nil, and there isn't a connected controller with same playerIndex, show controller view.
|
||||
if let index = Settings.localControllerPlayerIndex, !ExternalGameControllerManager.shared.connectedControllers.contains { $0.playerIndex == index }
|
||||
if let index = Settings.localControllerPlayerIndex, !ExternalGameControllerManager.shared.connectedControllers.contains(where: { $0.playerIndex == index })
|
||||
{
|
||||
self.controllerView.playerIndex = index
|
||||
self.controllerView.isHidden = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.controllerView.playerIndex = nil
|
||||
self.controllerView.isHidden = true
|
||||
|
||||
if let game = self.game,
|
||||
let traits = self.controllerView.controllerSkinTraits,
|
||||
let controllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type),
|
||||
controllerSkin.hasTouchScreen(for: traits)
|
||||
{
|
||||
self.controllerView.isHidden = false
|
||||
self.controllerView.playerIndex = 0
|
||||
}
|
||||
else
|
||||
{
|
||||
self.controllerView.isHidden = true
|
||||
self.controllerView.playerIndex = nil
|
||||
}
|
||||
|
||||
Settings.localControllerPlayerIndex = nil
|
||||
}
|
||||
|
||||
@ -518,16 +591,19 @@ private extension GameViewController
|
||||
{
|
||||
if gameController.playerIndex != nil
|
||||
{
|
||||
if let inputMapping = GameControllerInputMapping.inputMapping(for: gameController, gameType: game.type, in: DatabaseManager.shared.viewContext)
|
||||
let inputMapping: GameControllerInputMappingProtocol
|
||||
|
||||
if let mapping = GameControllerInputMapping.inputMapping(for: gameController, gameType: game.type, in: DatabaseManager.shared.viewContext)
|
||||
{
|
||||
gameController.addReceiver(self, inputMapping: inputMapping)
|
||||
gameController.addReceiver(emulatorCore, inputMapping: inputMapping)
|
||||
inputMapping = mapping
|
||||
}
|
||||
else
|
||||
{
|
||||
gameController.addReceiver(self)
|
||||
gameController.addReceiver(emulatorCore)
|
||||
inputMapping = DefaultInputMapping(gameController: gameController)
|
||||
}
|
||||
|
||||
gameController.addReceiver(self, inputMapping: inputMapping)
|
||||
gameController.addReceiver(emulatorCore, inputMapping: inputMapping)
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -552,18 +628,36 @@ private extension GameViewController
|
||||
|
||||
self.controllerView.isButtonHapticFeedbackEnabled = Settings.isButtonHapticFeedbackEnabled
|
||||
self.controllerView.isThumbstickHapticFeedbackEnabled = Settings.isThumbstickHapticFeedbackEnabled
|
||||
|
||||
self.updateControllerSkin()
|
||||
}
|
||||
|
||||
func updateControllerSkin()
|
||||
{
|
||||
guard let game = self.game, let system = System(gameType: game.type), let window = self.view.window else { return }
|
||||
guard let game = self.game as? Game, let window = self.view.window else { return }
|
||||
|
||||
let traits = DeltaCore.ControllerSkin.Traits.defaults(for: window)
|
||||
|
||||
let controllerSkin = Settings.preferredControllerSkin(for: system, traits: traits)
|
||||
self.controllerView.controllerSkin = controllerSkin
|
||||
if Settings.localControllerPlayerIndex != nil
|
||||
{
|
||||
let controllerSkin = Settings.preferredControllerSkin(for: game, traits: traits)
|
||||
self.controllerView.controllerSkin = controllerSkin
|
||||
}
|
||||
else if let controllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), controllerSkin.hasTouchScreen(for: traits)
|
||||
{
|
||||
var touchControllerSkin = TouchControllerSkin(controllerSkin: controllerSkin)
|
||||
touchControllerSkin.layoutGuide = self.view.safeAreaLayoutGuide
|
||||
|
||||
switch traits.orientation
|
||||
{
|
||||
case .portrait: touchControllerSkin.screenLayoutAxis = .vertical
|
||||
case .landscape: touchControllerSkin.screenLayoutAxis = .horizontal
|
||||
}
|
||||
|
||||
self.controllerView.controllerSkin = touchControllerSkin
|
||||
}
|
||||
|
||||
self.view.setNeedsUpdateConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@ -620,6 +714,8 @@ extension GameViewController: SaveStatesViewControllerDelegate
|
||||
// Ensures game is non-nil and also a Game subclass
|
||||
guard let game = self.game as? Game else { return }
|
||||
|
||||
guard let emulatorCore = self.emulatorCore, emulatorCore.state != .stopped else { return }
|
||||
|
||||
// If pausedSaveState exists and has already been saved, don't update auto save state
|
||||
// This prevents us from filling our auto save state slots with the same save state
|
||||
let savedPausedSaveState = self.pausedSaveState?.isSaved ?? false
|
||||
@ -699,7 +795,7 @@ extension GameViewController: SaveStatesViewControllerDelegate
|
||||
self.emulatorCore?.saveSaveState(to: saveState.fileURL)
|
||||
}
|
||||
|
||||
if let snapshot = self.gameView.snapshot(), let data = snapshot.pngData()
|
||||
if let snapshot = self.emulatorCore?.videoManager.snapshot(), let data = snapshot.pngData()
|
||||
{
|
||||
do
|
||||
{
|
||||
@ -712,6 +808,7 @@ extension GameViewController: SaveStatesViewControllerDelegate
|
||||
}
|
||||
|
||||
saveState.modifiedDate = Date()
|
||||
saveState.coreIdentifier = self.emulatorCore?.deltaCore.identifier
|
||||
|
||||
if isRunning
|
||||
{
|
||||
@ -980,6 +1077,16 @@ extension GameViewController: GameViewControllerDelegate
|
||||
}
|
||||
}
|
||||
|
||||
private extension GameViewController
|
||||
{
|
||||
func show(_ toastView: RSTToastView, duration: TimeInterval = 3.0)
|
||||
{
|
||||
toastView.textLabel.textAlignment = .center
|
||||
toastView.presentationEdge = .top
|
||||
toastView.show(in: self.view, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notifications -
|
||||
private extension GameViewController
|
||||
{
|
||||
@ -1078,16 +1185,7 @@ private extension GameViewController
|
||||
func presentToastView()
|
||||
{
|
||||
let toastView = RSTToastView(text: NSLocalizedString("Autorotation Disabled", comment: ""), detailText: NSLocalizedString("Pause game to change orientation.", comment: ""))
|
||||
toastView.textLabel.textAlignment = .center
|
||||
toastView.presentationEdge = .bottom
|
||||
|
||||
if let traits = self.controllerView.controllerSkinTraits, traits.orientation == .landscape, self.controllerView?.controllerSkin?.gameScreenFrame(for: traits) == nil
|
||||
{
|
||||
// Only change landscape vertical offset if there is no custom game screen frame for the current controller skin.
|
||||
toastView.edgeOffset.vertical = 30
|
||||
}
|
||||
|
||||
toastView.show(in: self.gameView, duration: 3.0)
|
||||
self.show(toastView)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@ -1108,4 +1206,29 @@ private extension GameViewController
|
||||
{
|
||||
self.isGyroActive = false
|
||||
}
|
||||
|
||||
@objc func emulationDidQuit(with notification: Notification)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
guard self.presentedViewController == nil else { return }
|
||||
|
||||
// Wait for emulation to stop completely before performing segue.
|
||||
var token: NSKeyValueObservation?
|
||||
token = self.emulatorCore?.observe(\.state, options: [.initial]) { (emulatorCore, change) in
|
||||
guard emulatorCore.state == .stopped else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.game = nil
|
||||
self.performSegue(withIdentifier: "showGamesViewController", sender: nil)
|
||||
}
|
||||
|
||||
token?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@NSManaged var desmumeDeprecatedAlertCount: Int
|
||||
}
|
||||
|
||||
@ -27,7 +27,16 @@ class PreviewGameViewController: DeltaCore.GameViewController
|
||||
}
|
||||
}
|
||||
|
||||
var isLivePreview: Bool = true
|
||||
|
||||
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
|
||||
private var copiedSaveFiles = [(originalURL: URL, copyURL: URL)]()
|
||||
|
||||
private lazy var temporaryDirectoryURL: URL = {
|
||||
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent("preview-" + UUID().uuidString)
|
||||
try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
return directoryURL
|
||||
}()
|
||||
|
||||
override var game: GameProtocol? {
|
||||
willSet {
|
||||
@ -41,7 +50,8 @@ class PreviewGameViewController: DeltaCore.GameViewController
|
||||
|
||||
emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
|
||||
|
||||
self.preferredContentSize = emulatorCore.preferredRenderingSize
|
||||
let size = CGSize(width: emulatorCore.preferredRenderingSize.width * 2.0, height: emulatorCore.preferredRenderingSize.height * 2.0)
|
||||
self.preferredContentSize = size
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,8 +60,26 @@ class PreviewGameViewController: DeltaCore.GameViewController
|
||||
return previewActionItems
|
||||
}
|
||||
|
||||
public required init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
deinit
|
||||
{
|
||||
// Explicitly stop emulatorCore _before_ we remove ourselves as observer
|
||||
// so we can wait until stopped before restoring save files (again).
|
||||
self.emulatorCore?.stop()
|
||||
|
||||
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
|
||||
}
|
||||
}
|
||||
@ -65,17 +93,25 @@ extension PreviewGameViewController
|
||||
super.viewDidLoad()
|
||||
|
||||
self.controllerView.isHidden = true
|
||||
self.controllerView.controllerSkin = nil // Skip loading controller skin from disk, which may be slow.
|
||||
|
||||
// Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches
|
||||
self.emulatorCore?.remove(self.gameView)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.copySaveFiles()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.emulatorCoreQueue.async {
|
||||
self.emulatorCore?.resume()
|
||||
self.startEmulation()
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +123,19 @@ extension PreviewGameViewController
|
||||
self.emulatorCore?.pause()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
// Already stopped = we've already restored save files and removed directory.
|
||||
if self.emulatorCore?.state != .stopped
|
||||
{
|
||||
// Pre-emptively restore save files in case something goes wrong while stopping emulation.
|
||||
// This also ensures if the core is never stopped (for some reason), saves are still restored.
|
||||
self.restoreSaveFiles(removeCopyDirectory: false)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
@ -111,17 +160,21 @@ extension PreviewGameViewController
|
||||
let state = self.emulatorCore?.state
|
||||
else { return }
|
||||
|
||||
if previousState == .stopped, state == .running
|
||||
switch state
|
||||
{
|
||||
self.emulatorCoreQueue.sync {
|
||||
if self.isAppearing
|
||||
{
|
||||
// Pause to prevent it from starting before visible (in case user peeked slowly)
|
||||
self.emulatorCore?.pause()
|
||||
}
|
||||
|
||||
case .running where previousState == .stopped:
|
||||
self.emulatorCoreQueue.async {
|
||||
// Pause to prevent it from starting before visible (in case user peeked slowly)
|
||||
self.emulatorCore?.pause()
|
||||
self.preparePreview()
|
||||
}
|
||||
|
||||
case .stopped:
|
||||
// Emulation has stopped, so we can safely restore save files,
|
||||
// and also remove the directory they were copied to.
|
||||
self.restoreSaveFiles(removeCopyDirectory: true)
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,5 +225,67 @@ private extension PreviewGameViewController
|
||||
|
||||
// Re-enable emulatorCore to update gameView again
|
||||
self.emulatorCore?.add(self.gameView)
|
||||
|
||||
self.emulatorCore?.resume()
|
||||
}
|
||||
|
||||
func copySaveFiles()
|
||||
{
|
||||
guard let game = self.game as? Game, let gameSave = game.gameSave else { return }
|
||||
|
||||
self.copiedSaveFiles.removeAll()
|
||||
|
||||
let fileURLs = gameSave.syncableFiles.lazy.map { $0.fileURL }
|
||||
for fileURL in fileURLs
|
||||
{
|
||||
do
|
||||
{
|
||||
let destinationURL = self.temporaryDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
self.copiedSaveFiles.append((fileURL, destinationURL))
|
||||
print("Copied save file:", fileURL.lastPathComponent)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to back up save file \(fileURL.lastPathComponent).", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreSaveFiles(removeCopyDirectory: Bool)
|
||||
{
|
||||
for (originalURL, copyURL) in self.copiedSaveFiles
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: copyURL, to: originalURL, shouldReplace: true)
|
||||
print("Restored save file:", originalURL.lastPathComponent)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to restore copied save file \(copyURL.lastPathComponent).", error)
|
||||
}
|
||||
}
|
||||
|
||||
if removeCopyDirectory
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectoryURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove preview temporary directory.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PreviewGameViewController: GameViewControllerDelegate
|
||||
{
|
||||
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
|
||||
{
|
||||
return self.isLivePreview
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,24 @@
|
||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
|
||||
extension ControllerSkin
|
||||
{
|
||||
convenience init?(system: System, context: NSManagedObjectContext)
|
||||
{
|
||||
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { return nil }
|
||||
|
||||
self.init(context: context)
|
||||
|
||||
self.isStandard = true
|
||||
self.filename = deltaControllerSkin.fileURL.lastPathComponent
|
||||
|
||||
self.configure(with: deltaControllerSkin)
|
||||
}
|
||||
|
||||
func configure(with skin: DeltaCore.ControllerSkin)
|
||||
{
|
||||
// Manually copy values to be stored in database.
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
//
|
||||
// DeltaCoreProtocol+Delta.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/30/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import DeltaCore
|
||||
|
||||
extension DeltaCoreProtocol
|
||||
{
|
||||
var supportedRates: ClosedRange<Double> {
|
||||
guard let system = System(gameType: self.gameType) else { return 1...1 }
|
||||
|
||||
switch system
|
||||
{
|
||||
case .nes: return 1...4
|
||||
case .snes: return 1...4
|
||||
case .gbc: return 1...4
|
||||
case .gba: return 1...3
|
||||
case .n64: return 1...3
|
||||
case .ds: return 1...1
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,4 +12,7 @@ extension HarmonyMetadataKey
|
||||
{
|
||||
static let gameID = HarmonyMetadataKey("gameID")
|
||||
static let gameName = HarmonyMetadataKey("gameName")
|
||||
|
||||
// Backwards compatibility
|
||||
static let coreID = HarmonyMetadataKey("coreID")
|
||||
}
|
||||
|
||||
@ -20,16 +20,6 @@ extension UIAlertController
|
||||
|
||||
class func alertController(for importType: ImportType, with errors: Set<DatabaseManager.ImportError>) -> UIAlertController
|
||||
{
|
||||
let title: String
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
|
||||
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
|
||||
var urls = Set<URL>()
|
||||
|
||||
for error in errors
|
||||
@ -44,38 +34,54 @@ extension UIAlertController
|
||||
}
|
||||
}
|
||||
|
||||
let filenames = urls.map{ $0.lastPathComponent }.sorted()
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
if filenames.count > 0
|
||||
if let fileURL = urls.first, let error = errors.first, errors.count == 1
|
||||
{
|
||||
var message: String
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: message = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n"
|
||||
case .controllerSkins: message = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n"
|
||||
}
|
||||
|
||||
for filename in filenames
|
||||
{
|
||||
message += "\n" + filename
|
||||
}
|
||||
|
||||
alertController.message = message
|
||||
title = String(format: NSLocalizedString("Could not import “%@”.", comment: ""), fileURL.lastPathComponent)
|
||||
message = error.localizedDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
// This branch can be executed when there are no input URLs when importing, but there is an error saving the database anyway.
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: alertController.message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "")
|
||||
case .controllerSkins: alertController.message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "")
|
||||
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
|
||||
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
|
||||
}
|
||||
|
||||
if urls.count > 0
|
||||
{
|
||||
var tempMessage: String
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: tempMessage = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n"
|
||||
case .controllerSkins: tempMessage = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n"
|
||||
}
|
||||
|
||||
let filenames = urls.map { $0.lastPathComponent }.sorted()
|
||||
for filename in filenames
|
||||
{
|
||||
tempMessage += "\n" + filename
|
||||
}
|
||||
|
||||
message = tempMessage
|
||||
}
|
||||
else
|
||||
{
|
||||
// This branch can be executed when there are no input URLs when importing, but there is an error saving the database anyway.
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "")
|
||||
case .controllerSkins: message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
|
||||
|
||||
return alertController
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,12 +8,31 @@
|
||||
|
||||
import UIKit
|
||||
import ARKit
|
||||
import Metal
|
||||
|
||||
extension UIDevice
|
||||
{
|
||||
private static var mtlDevice: MTLDevice? = MTLCreateSystemDefaultDevice()
|
||||
|
||||
var hasA9ProcessorOrBetter: Bool {
|
||||
// ARKit is only supported by devices with an A9 processor or better, according to the documentation.
|
||||
// https://developer.apple.com/documentation/arkit/arconfiguration/2923553-issupported
|
||||
return ARConfiguration.isSupported
|
||||
}
|
||||
|
||||
var hasA11ProcessorOrBetter: Bool {
|
||||
guard let mtlDevice = UIDevice.mtlDevice else { return false }
|
||||
return mtlDevice.supportsFeatureSet(.iOS_GPUFamily4_v1) // iOS GPU Family 4 = A11 GPU
|
||||
}
|
||||
|
||||
var supportsJIT: Bool {
|
||||
// As of iOS 14.4 beta 2, JIT is no longer supported :(
|
||||
// Hopefully this change is reversed before the public release...
|
||||
let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0)
|
||||
guard #available(iOS 14.2, *), !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4) else { return false }
|
||||
|
||||
// JIT is supported on devices with an A12 processor or better running iOS 14.2 or later.
|
||||
// ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
|
||||
return ARBodyTrackingConfiguration.isSupported
|
||||
}
|
||||
}
|
||||
|
||||
24
Delta/Extensions/UIImage+SymbolFallback.swift
Normal file
24
Delta/Extensions/UIImage+SymbolFallback.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// UIImage+SymbolFallback.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 2/5/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIImage
|
||||
{
|
||||
convenience init?(symbolNameIfAvailable name: String)
|
||||
{
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
self.init(systemName: name)
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,10 @@
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import AVFoundation
|
||||
|
||||
import DeltaCore
|
||||
import MelonDSDeltaCore
|
||||
|
||||
import Roxas
|
||||
import Harmony
|
||||
@ -22,6 +24,7 @@ extension GameCollectionViewController
|
||||
{
|
||||
case alreadyRunning
|
||||
case downloadingGameSave
|
||||
case biosNotFound
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,12 +62,14 @@ class GameCollectionViewController: UICollectionViewController
|
||||
|
||||
private let prototypeCell = GridCollectionViewCell()
|
||||
|
||||
private var _performing3DTouchTransition = false
|
||||
private weak var _destination3DTouchTransitionViewController: UIViewController?
|
||||
private var _performingPreviewTransition = false
|
||||
private weak var _previewTransitionViewController: PreviewGameViewController?
|
||||
private weak var _previewTransitionDestinationViewController: UIViewController?
|
||||
|
||||
private var _renameAction: UIAlertAction?
|
||||
private var _changingArtworkGame: Game?
|
||||
private var _importingSaveFileGame: Game?
|
||||
private var _exportedSaveFileURL: URL?
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
@ -92,27 +97,31 @@ extension GameCollectionViewController
|
||||
layout.itemWidth = 90
|
||||
layout.minimumInteritemSpacing = 12
|
||||
|
||||
self.registerForPreviewing(with: self, sourceView: self.collectionView!)
|
||||
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:)))
|
||||
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
|
||||
if #available(iOS 13, *) {}
|
||||
else
|
||||
{
|
||||
self.registerForPreviewing(with: self, sourceView: self.collectionView!)
|
||||
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:)))
|
||||
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if _performing3DTouchTransition
|
||||
if _performingPreviewTransition
|
||||
{
|
||||
_performing3DTouchTransition = false
|
||||
_performingPreviewTransition = false
|
||||
|
||||
// Unlike our custom transitions, 3D Touch transition doesn't manually call appearance methods for us
|
||||
// To compensate, we call them ourselves
|
||||
_destination3DTouchTransitionViewController?.beginAppearanceTransition(true, animated: true)
|
||||
_previewTransitionDestinationViewController?.beginAppearanceTransition(true, animated: true)
|
||||
|
||||
self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in
|
||||
self._destination3DTouchTransitionViewController?.endAppearanceTransition()
|
||||
self._destination3DTouchTransitionViewController = nil
|
||||
self._previewTransitionDestinationViewController?.endAppearanceTransition()
|
||||
self._previewTransitionDestinationViewController = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -142,6 +151,12 @@ extension GameCollectionViewController
|
||||
saveStatesViewController.game = game
|
||||
saveStatesViewController.mode = .loading
|
||||
saveStatesViewController.theme = self.theme
|
||||
|
||||
case "preferredControllerSkins":
|
||||
let game = sender as! Game
|
||||
|
||||
let preferredControllerSkinsViewController = (segue.destination as! UINavigationController).topViewController as! PreferredControllerSkinsViewController
|
||||
preferredControllerSkinsViewController.game = game
|
||||
|
||||
case "unwindFromGames":
|
||||
let destinationViewController = segue.destination as! GameViewController
|
||||
@ -152,6 +167,22 @@ extension GameCollectionViewController
|
||||
|
||||
destinationViewController.game = game
|
||||
|
||||
if let emulatorBridge = destinationViewController.emulatorCore?.deltaCore.emulatorBridge as? MelonDSEmulatorBridge
|
||||
{
|
||||
//TODO: Update this to work with multiple processes by retrieving emulatorBridge directly from emulatorCore.
|
||||
|
||||
if game.identifier == Game.melonDSDSiBIOSIdentifier
|
||||
{
|
||||
emulatorBridge.systemType = .dsi
|
||||
}
|
||||
else
|
||||
{
|
||||
emulatorBridge.systemType = .ds
|
||||
}
|
||||
|
||||
emulatorBridge.isJITEnabled = UIDevice.current.supportsJIT
|
||||
}
|
||||
|
||||
if let saveState = self.activeSaveState
|
||||
{
|
||||
// Must be synchronous or else there will be a flash of black
|
||||
@ -176,20 +207,16 @@ extension GameCollectionViewController
|
||||
|
||||
self.activeSaveState = nil
|
||||
|
||||
if _performing3DTouchTransition
|
||||
if _performingPreviewTransition
|
||||
{
|
||||
_destination3DTouchTransitionViewController = destinationViewController
|
||||
_previewTransitionDestinationViewController = destinationViewController
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction private func unwindFromSaveStatesViewController(with segue: UIStoryboardSegue)
|
||||
{
|
||||
}
|
||||
|
||||
@IBAction private func unwindFromGamesDatabaseBrowser(with segue: UIStoryboardSegue)
|
||||
@IBAction private func unwindToGameCollectionViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -260,6 +287,7 @@ private extension GameCollectionViewController
|
||||
cell.maximumImageSize = CGSize(width: 90, height: 90)
|
||||
cell.textLabel.text = game.name
|
||||
cell.textLabel.textColor = UIColor.gray
|
||||
cell.tintColor = cell.textLabel.textColor
|
||||
}
|
||||
|
||||
//MARK: - Emulation
|
||||
@ -320,6 +348,16 @@ private extension GameCollectionViewController
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
catch LaunchError.biosNotFound
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Missing Required DS Files", comment: ""), message: NSLocalizedString("Delta requires certain files to play Nintendo DS games. Please import them to launch this game.", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Import Files", comment: ""), style: .default) { _ in
|
||||
self.performSegue(withIdentifier: "showDSSettings", sender: nil)
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Launch Game", comment: ""), error: error)
|
||||
@ -370,6 +408,27 @@ private extension GameCollectionViewController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if game.type == .ds && Settings.preferredCore(for: .ds) == MelonDS.core
|
||||
{
|
||||
if game.identifier == Game.melonDSDSiBIOSIdentifier
|
||||
{
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS7URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS9URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiFirmwareURL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiNANDURL.path)
|
||||
else { throw LaunchError.biosNotFound }
|
||||
}
|
||||
else
|
||||
{
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios7URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios9URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.firmwareURL.path)
|
||||
else { throw LaunchError.biosNotFound }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -380,34 +439,46 @@ private extension GameCollectionViewController
|
||||
{
|
||||
let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil)
|
||||
|
||||
let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "pencil.and.ellipsis.rectangle"), action: { [unowned self] action in
|
||||
self.rename(game)
|
||||
})
|
||||
|
||||
let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default) { [unowned self] action in
|
||||
let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "photo")) { [unowned self] action in
|
||||
self.changeArtwork(for: game)
|
||||
}
|
||||
|
||||
let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let changeControllerSkinAction = Action(title: NSLocalizedString("Change Controller Skin", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "gamecontroller")) { [unowned self] _ in
|
||||
self.changePreferredControllerSkin(for: game)
|
||||
}
|
||||
|
||||
let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "square.and.arrow.up"), action: { [unowned self] action in
|
||||
self.share(game)
|
||||
})
|
||||
|
||||
let saveStatesAction = Action(title: NSLocalizedString("Save States", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let saveStatesAction = Action(title: NSLocalizedString("Save States", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "doc.on.doc"), action: { [unowned self] action in
|
||||
self.viewSaveStates(for: game)
|
||||
})
|
||||
|
||||
let importSaveFile = Action(title: NSLocalizedString("Import Save File", comment: ""), style: .default) { [unowned self] _ in
|
||||
let importSaveFile = Action(title: NSLocalizedString("Import Save File", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "tray.and.arrow.down")) { [unowned self] _ in
|
||||
self.importSaveFile(for: game)
|
||||
}
|
||||
|
||||
let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, action: { [unowned self] action in
|
||||
let exportSaveFile = Action(title: NSLocalizedString("Export Save File", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "tray.and.arrow.up")) { [unowned self] _ in
|
||||
self.exportSaveFile(for: game)
|
||||
}
|
||||
|
||||
let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, image: UIImage(symbolNameIfAvailable: "trash"), action: { [unowned self] action in
|
||||
self.delete(game)
|
||||
})
|
||||
|
||||
switch game.type
|
||||
{
|
||||
case GameType.unknown: return [cancelAction, renameAction, changeArtworkAction, shareAction, deleteAction]
|
||||
default: return [cancelAction, renameAction, changeArtworkAction, shareAction, saveStatesAction, importSaveFile, deleteAction]
|
||||
case GameType.unknown:
|
||||
return [cancelAction, renameAction, changeArtworkAction, shareAction, deleteAction]
|
||||
case .ds where game.identifier == Game.melonDSBIOSIdentifier || game.identifier == Game.melonDSDSiBIOSIdentifier:
|
||||
return [cancelAction, renameAction, changeArtworkAction, changeControllerSkinAction, saveStatesAction]
|
||||
default:
|
||||
return [cancelAction, renameAction, changeArtworkAction, changeControllerSkinAction, shareAction, saveStatesAction, importSaveFile, exportSaveFile, deleteAction]
|
||||
}
|
||||
}
|
||||
|
||||
@ -488,6 +559,13 @@ private extension GameCollectionViewController
|
||||
|
||||
func changeArtwork(for game: Game, toImageAt url: URL?, errors: [Error])
|
||||
{
|
||||
defer {
|
||||
if let temporaryImageURL = url
|
||||
{
|
||||
try? FileManager.default.removeItem(at: temporaryImageURL)
|
||||
}
|
||||
}
|
||||
|
||||
var errors = errors
|
||||
|
||||
var imageURL: URL?
|
||||
@ -503,7 +581,8 @@ private extension GameCollectionViewController
|
||||
if
|
||||
let image = UIImage(data: imageData),
|
||||
let resizedImage = image.resizing(toFit: CGSize(width: 300, height: 300)),
|
||||
let resizedData = resizedImage.jpegData(compressionQuality: 0.85)
|
||||
let rotatedImage = resizedImage.rotatedToIntrinsicOrientation(), // in case image was imported directly from Files
|
||||
let resizedData = rotatedImage.pngData()
|
||||
{
|
||||
let destinationURL = DatabaseManager.artworkURL(for: game)
|
||||
try resizedData.write(to: destinationURL, options: .atomic)
|
||||
@ -529,6 +608,14 @@ private extension GameCollectionViewController
|
||||
|
||||
if let imageURL = imageURL
|
||||
{
|
||||
self.dataSource.prefetchItemCache.removeObject(forKey: game)
|
||||
|
||||
if let cacheManager = SDWebImageManager.shared()
|
||||
{
|
||||
let cacheKey = cacheManager.cacheKey(for: imageURL)
|
||||
cacheManager.imageCache.removeImage(forKey: cacheKey)
|
||||
}
|
||||
|
||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||
let temporaryGame = context.object(with: game.objectID) as! Game
|
||||
temporaryGame.artworkURL = imageURL
|
||||
@ -538,6 +625,13 @@ private extension GameCollectionViewController
|
||||
SyncManager.shared.recordController?.updateRecord(for: temporaryGame)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)
|
||||
{
|
||||
// Manually reload item because collection view may not be in window hierarchy,
|
||||
// which means it won't automatically update when we save the context.
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
|
||||
self.presentedViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@ -649,6 +743,34 @@ private extension GameCollectionViewController
|
||||
}
|
||||
}
|
||||
|
||||
func exportSaveFile(for game: Game)
|
||||
{
|
||||
do
|
||||
{
|
||||
let illegalCharacterSet = CharacterSet(charactersIn: "\"\\/?<>:*|")
|
||||
let sanitizedFilename = game.name.components(separatedBy: illegalCharacterSet).joined() + "." + game.gameSaveURL.pathExtension
|
||||
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename)
|
||||
try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true)
|
||||
|
||||
self._exportedSaveFileURL = temporaryURL
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(urls: [temporaryURL], in: .exportToService)
|
||||
documentPicker.delegate = self
|
||||
self.present(documentPicker, animated: true, completion: nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Export Save File", comment: ""), error: error)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func changePreferredControllerSkin(for game: Game)
|
||||
{
|
||||
self.performSegue(withIdentifier: "preferredControllerSkins", sender: game)
|
||||
}
|
||||
|
||||
@objc func textFieldTextDidChange(_ textField: UITextField)
|
||||
{
|
||||
let text = textField.text ?? ""
|
||||
@ -687,6 +809,18 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
|
||||
|
||||
let game = self.dataSource.item(at: indexPath)
|
||||
|
||||
let gameViewController = self.makePreviewGameViewController(for: game)
|
||||
_previewTransitionViewController = gameViewController
|
||||
return gameViewController
|
||||
}
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||
{
|
||||
self.commitPreviewTransition()
|
||||
}
|
||||
|
||||
func makePreviewGameViewController(for game: Game) -> PreviewGameViewController
|
||||
{
|
||||
let gameViewController = PreviewGameViewController()
|
||||
gameViewController.game = game
|
||||
|
||||
@ -696,37 +830,63 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
|
||||
gameViewController.previewImage = UIImage(contentsOfFile: previewSaveState.imageFileURL.path)
|
||||
}
|
||||
|
||||
if let emulatorBridge = gameViewController.emulatorCore?.deltaCore.emulatorBridge as? MelonDSEmulatorBridge
|
||||
{
|
||||
//TODO: Update this to work with multiple processes by retrieving emulatorBridge directly from emulatorCore.
|
||||
|
||||
if game.identifier == Game.melonDSDSiBIOSIdentifier
|
||||
{
|
||||
emulatorBridge.systemType = .dsi
|
||||
}
|
||||
else
|
||||
{
|
||||
emulatorBridge.systemType = .ds
|
||||
}
|
||||
|
||||
emulatorBridge.isJITEnabled = UIDevice.current.supportsJIT
|
||||
}
|
||||
|
||||
let actions = self.actions(for: game).previewActions
|
||||
gameViewController.overridePreviewActionItems = actions
|
||||
|
||||
return gameViewController
|
||||
}
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||
func commitPreviewTransition()
|
||||
{
|
||||
let gameViewController = viewControllerToCommit as! PreviewGameViewController
|
||||
let game = gameViewController.game as! Game
|
||||
guard let gameViewController = _previewTransitionViewController else { return }
|
||||
|
||||
let game = gameViewController.game as! Game
|
||||
gameViewController.pauseEmulation()
|
||||
|
||||
let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)!
|
||||
|
||||
let fileURL = FileManager.default.uniqueTemporaryURL()
|
||||
self.activeSaveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL)
|
||||
|
||||
if gameViewController.isLivePreview
|
||||
{
|
||||
self.activeSaveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.activeSaveState = gameViewController.previewSaveState
|
||||
}
|
||||
|
||||
gameViewController.emulatorCore?.stop()
|
||||
|
||||
_performing3DTouchTransition = true
|
||||
_performingPreviewTransition = true
|
||||
|
||||
self.launchGame(at: indexPath, clearScreen: true, ignoreAlreadyRunningError: true)
|
||||
|
||||
do
|
||||
if gameViewController.isLivePreview
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -805,3 +965,91 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension GameCollectionViewController
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
|
||||
{
|
||||
let game = self.dataSource.item(at: indexPath)
|
||||
let actions = self.actions(for: game)
|
||||
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in
|
||||
guard let self = self else { return nil }
|
||||
|
||||
do
|
||||
{
|
||||
try self.validateLaunchingGame(game, ignoringErrors: [LaunchError.alreadyRunning])
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error trying to preview game:", error)
|
||||
return nil
|
||||
}
|
||||
|
||||
let previewViewController = self.makePreviewGameViewController(for: game)
|
||||
previewViewController.isLivePreview = Settings.isPreviewsEnabled
|
||||
|
||||
guard previewViewController.isLivePreview || previewViewController.previewSaveState != nil else { return nil }
|
||||
self._previewTransitionViewController = previewViewController
|
||||
|
||||
return previewViewController
|
||||
}) { suggestedActions in
|
||||
return UIMenu(title: game.name, children: actions.menuActions)
|
||||
}
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating)
|
||||
{
|
||||
self.commitPreviewTransition()
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||
{
|
||||
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
|
||||
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? GridCollectionViewCell else { return nil }
|
||||
|
||||
let parameters = UIPreviewParameters()
|
||||
parameters.backgroundColor = .clear
|
||||
|
||||
if let image = cell.imageView.image
|
||||
{
|
||||
let artworkFrame = AVMakeRect(aspectRatio: image.size, insideRect: cell.imageView.bounds)
|
||||
|
||||
let bezierPath = UIBezierPath(rect: artworkFrame)
|
||||
parameters.visiblePath = bezierPath
|
||||
}
|
||||
|
||||
let preview = UITargetedPreview(view: cell.imageView, parameters: parameters)
|
||||
return preview
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||
{
|
||||
_previewTransitionViewController = nil
|
||||
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
extension GameCollectionViewController: UIDocumentPickerDelegate
|
||||
{
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])
|
||||
{
|
||||
if let saveFileURL = self._exportedSaveFileURL
|
||||
{
|
||||
try? FileManager.default.removeItem(at: saveFileURL)
|
||||
}
|
||||
|
||||
self._exportedSaveFileURL = nil
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
|
||||
{
|
||||
if let saveFileURL = self._exportedSaveFileURL
|
||||
{
|
||||
try? FileManager.default.removeItem(at: saveFileURL)
|
||||
}
|
||||
|
||||
self._exportedSaveFileURL = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +75,8 @@ class GamesViewController: UIViewController
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.syncingDidStart(_:)), name: SyncCoordinator.didStartSyncingNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.settingsDidChange(_:)), name: .settingsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.emulationDidQuit(_:)), name: EmulatorCore.emulationDidQuitNotification, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,14 +356,18 @@ extension GamesViewController: ImportControllerDelegate
|
||||
{
|
||||
var documentTypes = Set(System.registeredSystems.map { $0.gameType.rawValue })
|
||||
documentTypes.insert(kUTTypeZipArchive as String)
|
||||
documentTypes.insert("com.rileytestut.delta.skin")
|
||||
|
||||
#if BETA
|
||||
// .bin files (Genesis ROMs)
|
||||
documentTypes.insert("com.apple.macbinary-archive")
|
||||
#endif
|
||||
|
||||
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
|
||||
documentTypes.insert("com.rileytestut.gba")
|
||||
documentTypes.insert("com.rileytestut.gbc")
|
||||
documentTypes.insert("com.rileytestut.gb")
|
||||
|
||||
documentTypes.insert("com.rileytestut.delta.skin")
|
||||
|
||||
let itunesImportOption = iTunesImportOption(presentingViewController: self)
|
||||
|
||||
let importController = ImportController(documentTypes: documentTypes)
|
||||
@ -469,6 +475,23 @@ private extension GamesViewController
|
||||
let navigationController = SyncResultViewController.make(result: result)
|
||||
self.present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func quitEmulation()
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.activeEmulatorCore = nil
|
||||
|
||||
if let viewControllers = self.pageViewController.viewControllers as? [GameCollectionViewController]
|
||||
{
|
||||
for collectionViewController in viewControllers
|
||||
{
|
||||
collectionViewController.activeEmulatorCore = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.theme = .opaque
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notifications -
|
||||
@ -483,16 +506,12 @@ private extension GamesViewController
|
||||
{
|
||||
if deletedObjects.contains(game)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.theme = .opaque
|
||||
}
|
||||
self.quitEmulation()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.theme = .opaque
|
||||
}
|
||||
self.quitEmulation()
|
||||
}
|
||||
}
|
||||
|
||||
@ -510,6 +529,27 @@ private extension GamesViewController
|
||||
self.showSyncFinishedToastView(result: result)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func emulationDidQuit(_ notification: Notification)
|
||||
{
|
||||
self.quitEmulation()
|
||||
}
|
||||
|
||||
@objc func settingsDidChange(_ notification: Notification)
|
||||
{
|
||||
guard let emulatorCore = self.activeEmulatorCore else { return }
|
||||
guard let game = emulatorCore.game as? Game else { return }
|
||||
|
||||
game.managedObjectContext?.performAndWait {
|
||||
guard
|
||||
let name = notification.userInfo?[Settings.NotificationUserInfoKey.name] as? String, name == Settings.preferredCoreSettingsKey(for: emulatorCore.game.type),
|
||||
let core = notification.userInfo?[Settings.NotificationUserInfoKey.core] as? DeltaCoreProtocol, core != emulatorCore.deltaCore
|
||||
else { return }
|
||||
|
||||
emulatorCore.stop()
|
||||
self.quitEmulation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UIPageViewController -
|
||||
|
||||
@ -19,8 +19,11 @@ struct ClipboardImportOption: ImportOption
|
||||
func `import`(withCompletionHandler completionHandler: @escaping (Set<URL>?) -> Void)
|
||||
{
|
||||
guard UIPasteboard.general.hasImages else { return completionHandler([]) }
|
||||
|
||||
guard let data = UIPasteboard.general.data(forPasteboardType: kUTTypeImage as String) else { return completionHandler([]) }
|
||||
|
||||
guard let image = UIPasteboard.general.image,
|
||||
let rotatedImage = image.rotatedToIntrinsicOrientation(),
|
||||
let data = rotatedImage.pngData()
|
||||
else { return completionHandler([]) }
|
||||
|
||||
do
|
||||
{
|
||||
|
||||
@ -42,7 +42,7 @@ extension PhotoLibraryImportOption: UIImagePickerControllerDelegate, UINavigatio
|
||||
{
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])
|
||||
{
|
||||
guard let image = info[.originalImage] as? UIImage, let data = image.jpegData(compressionQuality: 0.85) else {
|
||||
guard let image = info[.originalImage] as? UIImage, let rotatedImage = image.rotatedToIntrinsicOrientation(), let data = rotatedImage.pngData() else {
|
||||
self.completionHandler?([])
|
||||
return
|
||||
}
|
||||
|
||||
@ -131,6 +131,39 @@ class ImportController: NSObject
|
||||
}
|
||||
}
|
||||
|
||||
extension ImportController
|
||||
{
|
||||
func importExternalFile(at fileURL: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.readingIntent(with: fileURL)
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
else
|
||||
{
|
||||
// User intent.url, not url, as the system may have updated it when requesting access.
|
||||
guard intent.url.startAccessingSecurityScopedResource() else { throw CocoaError.error(.fileReadNoPermission) }
|
||||
defer { intent.url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
// Use url, not intent.url, to ensure the file name matches what was in the document browser.
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: intent.url, to: temporaryURL, shouldReplace: true)
|
||||
|
||||
completionHandler(.success(temporaryURL))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImportController: UIDocumentBrowserViewControllerDelegate
|
||||
{
|
||||
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL])
|
||||
@ -144,31 +177,11 @@ extension ImportController: UIDocumentBrowserViewControllerDelegate
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url)
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in
|
||||
|
||||
do
|
||||
self.importExternalFile(at: url) { (result) in
|
||||
switch result
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
else
|
||||
{
|
||||
// User intent.url, not url, as the system may have updated it when requesting access.
|
||||
guard intent.url.startAccessingSecurityScopedResource() else { throw CocoaError.error(.fileReadNoPermission) }
|
||||
defer { intent.url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
// Use url, not intent.url, to ensure the file name matches what was in the document browser.
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: intent.url, to: temporaryURL, shouldReplace: true)
|
||||
|
||||
coordinatedURLs.insert(temporaryURL)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
errors.append(error)
|
||||
case .failure(let error): errors.append(error)
|
||||
case .success(let fileURL): coordinatedURLs.insert(fileURL)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
|
||||
@ -183,9 +183,12 @@ extension EditCheatViewController
|
||||
if let superview = self.codeTextView.superview
|
||||
{
|
||||
let layoutMargins = superview.layoutMargins
|
||||
self.codeTextView.textContainerInset = layoutMargins
|
||||
|
||||
self.codeTextView.textContainer.lineFragmentPadding = 0
|
||||
if self.codeTextView.textContainerInset.left != layoutMargins.left
|
||||
{
|
||||
self.codeTextView.textContainerInset.left = layoutMargins.left // Don't change right inset because CheatTextView adjusts it as well.
|
||||
self.codeTextView.textContainer.lineFragmentPadding = 0
|
||||
self.codeTextView.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
if self.isAppearing && !self.isPreviewing
|
||||
@ -252,6 +255,8 @@ private extension EditCheatViewController
|
||||
|
||||
self.tableView.endUpdates()
|
||||
}
|
||||
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
func updateSaveButtonState()
|
||||
|
||||
@ -66,6 +66,8 @@ class SaveStatesViewController: UICollectionViewController
|
||||
private var prototypeCellWidthConstraint: NSLayoutConstraint!
|
||||
private var prototypeHeader = SaveStatesCollectionHeaderView()
|
||||
|
||||
private weak var _previewTransitionViewController: PreviewGameViewController?
|
||||
|
||||
private let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>
|
||||
|
||||
private var emulatorCoreSaveState: SaveStateProtocol?
|
||||
@ -115,12 +117,16 @@ extension SaveStatesViewController
|
||||
self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth)
|
||||
self.prototypeCellWidthConstraint.isActive = true
|
||||
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(SaveStatesViewController.handleLongPressGesture(_:)))
|
||||
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
|
||||
|
||||
self.prepareEmulatorCoreSaveState()
|
||||
|
||||
self.registerForPreviewing(with: self, sourceView: self.collectionView!)
|
||||
if #available(iOS 13, *) {}
|
||||
else
|
||||
{
|
||||
self.registerForPreviewing(with: self, sourceView: self.collectionView!)
|
||||
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(SaveStatesViewController.handleLongPressGesture(_:)))
|
||||
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
|
||||
}
|
||||
|
||||
self.navigationController?.navigationBar.barStyle = .blackTranslucent
|
||||
self.navigationController?.toolbar.barStyle = .blackTranslucent
|
||||
@ -196,9 +202,17 @@ private extension SaveStatesViewController
|
||||
{
|
||||
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(SaveState.game), self.game)
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(SaveState.type), ascending: true), NSSortDescriptor(key: #keyPath(SaveState.creationDate), ascending: Settings.sortSaveStatesByOldestFirst)]
|
||||
|
||||
if let system = System(gameType: self.game.type)
|
||||
{
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(SaveState.game), self.game, #keyPath(SaveState.coreIdentifier), system.deltaCore.identifier)
|
||||
}
|
||||
else
|
||||
{
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(SaveState.game), self.game)
|
||||
}
|
||||
|
||||
self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(SaveState.type), cacheName: nil)
|
||||
}
|
||||
|
||||
@ -393,7 +407,18 @@ private extension SaveStatesViewController
|
||||
|
||||
func updatePreviewSaveState(_ saveState: SaveState?)
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Change Preview Save State?", comment: ""), message: NSLocalizedString("The Preview Save State is loaded whenever you preview this game from the Main Menu with 3D Touch. Are you sure you want to change it?", comment: ""), preferredStyle: .alert)
|
||||
let message: String
|
||||
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
message = NSLocalizedString("The Preview Save State is loaded whenever you long press this game from the Main Menu. Are you sure you want to change it?", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("The Preview Save State is loaded whenever you 3D Touch this game from the Main Menu. Are you sure you want to change it?", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Change Preview Save State?", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Change", comment: ""), style: .default, handler: { (action) in
|
||||
|
||||
@ -471,20 +496,31 @@ private extension SaveStatesViewController
|
||||
{
|
||||
guard saveState.type != .auto else { return nil }
|
||||
|
||||
let isPreviewAvailable: Bool
|
||||
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
isPreviewAvailable = true
|
||||
}
|
||||
else
|
||||
{
|
||||
isPreviewAvailable = (self.traitCollection.forceTouchCapability == .available)
|
||||
}
|
||||
|
||||
var actions = [Action]()
|
||||
|
||||
if self.traitCollection.forceTouchCapability == .available
|
||||
if isPreviewAvailable
|
||||
{
|
||||
if saveState.game?.previewSaveState != saveState
|
||||
{
|
||||
let previewAction = Action(title: NSLocalizedString("Set as Preview Save State", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let previewAction = Action(title: NSLocalizedString("Set as Preview Save State", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "eye.fill"), action: { [unowned self] action in
|
||||
self.updatePreviewSaveState(saveState)
|
||||
})
|
||||
actions.append(previewAction)
|
||||
}
|
||||
else
|
||||
{
|
||||
let previewAction = Action(title: NSLocalizedString("Remove as Preview Save State", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let previewAction = Action(title: NSLocalizedString("Remove as Preview Save State", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "eye.slash.fill"), action: { [unowned self] action in
|
||||
self.updatePreviewSaveState(nil)
|
||||
})
|
||||
actions.append(previewAction)
|
||||
@ -494,7 +530,7 @@ private extension SaveStatesViewController
|
||||
let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil)
|
||||
actions.append(cancelAction)
|
||||
|
||||
let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "pencil.and.ellipsis.rectangle"), action: { [unowned self] action in
|
||||
self.renameSaveState(saveState)
|
||||
})
|
||||
actions.append(renameAction)
|
||||
@ -504,19 +540,19 @@ private extension SaveStatesViewController
|
||||
case .auto: break
|
||||
case .quick: break
|
||||
case .general:
|
||||
let lockAction = Action(title: NSLocalizedString("Lock", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let lockAction = Action(title: NSLocalizedString("Lock", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "lock.fill"), action: { [unowned self] action in
|
||||
self.lockSaveState(saveState)
|
||||
})
|
||||
actions.append(lockAction)
|
||||
|
||||
case .locked:
|
||||
let unlockAction = Action(title: NSLocalizedString("Unlock", comment: ""), style: .default, action: { [unowned self] action in
|
||||
let unlockAction = Action(title: NSLocalizedString("Unlock", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "lock.open.fill"), action: { [unowned self] action in
|
||||
self.unlockSaveState(saveState)
|
||||
})
|
||||
actions.append(unlockAction)
|
||||
}
|
||||
|
||||
let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, action: { [unowned self] action in
|
||||
let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, image: UIImage(symbolNameIfAvailable: "trash"), action: { [unowned self] action in
|
||||
self.deleteSaveState(saveState)
|
||||
})
|
||||
actions.append(deleteAction)
|
||||
@ -608,21 +644,36 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate
|
||||
previewingContext.sourceRect = layoutAttributes.frame
|
||||
|
||||
let saveState = self.dataSource.item(at: indexPath)
|
||||
let actions = self.actionsForSaveState(saveState)?.previewActions ?? []
|
||||
let previewImage = self.dataSource.prefetchItemCache.object(forKey: saveState) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
|
||||
|
||||
let previewGameViewController = PreviewGameViewController()
|
||||
previewGameViewController.game = self.game
|
||||
previewGameViewController.overridePreviewActionItems = actions
|
||||
previewGameViewController.previewSaveState = saveState
|
||||
previewGameViewController.previewImage = previewImage
|
||||
let previewGameViewController = self.makePreviewGameViewController(for: saveState)
|
||||
_previewTransitionViewController = previewGameViewController
|
||||
|
||||
return previewGameViewController
|
||||
}
|
||||
|
||||
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||
{
|
||||
let gameViewController = viewControllerToCommit as! PreviewGameViewController
|
||||
self.commitPreviewTransition()
|
||||
}
|
||||
|
||||
func makePreviewGameViewController(for saveState: SaveState) -> PreviewGameViewController
|
||||
{
|
||||
let previewImage = self.dataSource.prefetchItemCache.object(forKey: saveState) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
|
||||
|
||||
let gameViewController = PreviewGameViewController()
|
||||
gameViewController.game = self.game
|
||||
gameViewController.previewSaveState = saveState
|
||||
gameViewController.previewImage = previewImage
|
||||
|
||||
let actions = self.actionsForSaveState(saveState)?.previewActions ?? []
|
||||
gameViewController.overridePreviewActionItems = actions
|
||||
|
||||
return gameViewController
|
||||
}
|
||||
|
||||
func commitPreviewTransition()
|
||||
{
|
||||
guard let gameViewController = self._previewTransitionViewController else { return }
|
||||
gameViewController.pauseEmulation()
|
||||
|
||||
let fileURL = FileManager.default.uniqueTemporaryURL()
|
||||
@ -708,3 +759,47 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension SaveStatesViewController
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
|
||||
{
|
||||
let saveState = self.dataSource.item(at: indexPath)
|
||||
guard let actions = self.actionsForSaveState(saveState) else { return nil }
|
||||
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in
|
||||
guard let self = self, Settings.isPreviewsEnabled else { return nil }
|
||||
|
||||
let previewGameViewController = self.makePreviewGameViewController(for: saveState)
|
||||
self._previewTransitionViewController = previewGameViewController
|
||||
|
||||
return previewGameViewController
|
||||
}) { suggestedActions in
|
||||
return UIMenu(title: saveState.localizedName, children: actions.menuActions)
|
||||
}
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating)
|
||||
{
|
||||
self.commitPreviewTransition()
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||
{
|
||||
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
|
||||
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? GridCollectionViewCell else { return nil }
|
||||
|
||||
let parameters = UIPreviewParameters()
|
||||
parameters.backgroundColor = .clear
|
||||
|
||||
let preview = UITargetedPreview(view: cell.imageView, parameters: parameters)
|
||||
return preview
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||
{
|
||||
self._previewTransitionViewController = nil
|
||||
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
import DeltaCore
|
||||
|
||||
@ -114,8 +115,8 @@ private extension AppIconShortcutsViewController
|
||||
self.dataSource.rowAnimation = .fade
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.text = NSLocalizedString("No App Icon Shortcuts", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("You can customize the shortcuts that appear when 3D Touching the app icon once you've added some games.", comment: "")
|
||||
placeholderView.textLabel.text = NSLocalizedString("No Home Screen Shortcuts", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("You can customize the shortcuts that appear when long-pressing the app icon once you've added some games.", comment: "")
|
||||
self.dataSource.placeholderView = placeholderView
|
||||
}
|
||||
|
||||
@ -129,10 +130,13 @@ private extension AppIconShortcutsViewController
|
||||
}
|
||||
|
||||
func configureGameCell(_ cell: GameTableViewCell, with game: Game, for indexPath: IndexPath)
|
||||
{
|
||||
cell.nameLabel.textColor = .darkText
|
||||
cell.backgroundColor = .white
|
||||
|
||||
{
|
||||
if #available(iOS 13.0, *) {
|
||||
cell.nameLabel?.textColor = .label
|
||||
} else {
|
||||
cell.nameLabel?.textColor = .darkText
|
||||
}
|
||||
|
||||
cell.nameLabel.text = game.name
|
||||
cell.artworkImageView.image = #imageLiteral(resourceName: "BoxArt")
|
||||
|
||||
@ -193,11 +197,9 @@ private extension AppIconShortcutsViewController
|
||||
func addShortcut(for game: Game)
|
||||
{
|
||||
guard self.shortcutsDataSource.items.count < 4 else { return }
|
||||
|
||||
guard !self.shortcutsDataSource.items.contains(game) else { return }
|
||||
|
||||
// No need to adjust destinationIndexPath, since it forwards change directly to table view.
|
||||
let destinationIndexPath = IndexPath(row: self.shortcutsDataSource.items.count, section: 1)
|
||||
let destinationIndexPath = IndexPath(row: self.shortcutsDataSource.items.count, section: 0)
|
||||
|
||||
let insertion = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: destinationIndexPath)
|
||||
insertion.rowAnimation = .fade
|
||||
@ -263,8 +265,8 @@ extension AppIconShortcutsViewController
|
||||
|
||||
switch (section, Settings.gameShortcutsMode)
|
||||
{
|
||||
case (0, .recent): return NSLocalizedString("Your most recently played games will appear as shortcuts when 3D touching the app icon.", comment: "")
|
||||
case (0, .manual): return NSLocalizedString("The games you've selected below will appear as shortcuts when 3D touching the app icon.", comment: "")
|
||||
case (0, .recent): return NSLocalizedString("Your most recently played games will appear as shortcuts when long-pressing the app icon.", comment: "")
|
||||
case (0, .manual): return NSLocalizedString("The games you've selected below will appear as shortcuts when long-pressing the app icon.", comment: "")
|
||||
case (1, .recent) where self.shortcutsDataSource.itemCount == 0: return NSLocalizedString("You have no recently played games.", comment: "")
|
||||
case (1, .recent): return " " // Return non-empty string since empty string changes vertical offset of section for some reason.
|
||||
case (1, .manual): return NSLocalizedString("You may have up to 4 shortcuts.", comment: "")
|
||||
@ -291,11 +293,13 @@ extension AppIconShortcutsViewController
|
||||
{
|
||||
case .none: break
|
||||
case .delete:
|
||||
let deletion = RSTCellContentChange(type: .delete, currentIndexPath: indexPath, destinationIndexPath: nil)
|
||||
let adjustedIndexPath = IndexPath(row: indexPath.row, section: 0)
|
||||
|
||||
let deletion = RSTCellContentChange(type: .delete, currentIndexPath: adjustedIndexPath, destinationIndexPath: nil)
|
||||
deletion.rowAnimation = .fade
|
||||
|
||||
var shortcuts = self.shortcutsDataSource.items
|
||||
shortcuts.remove(at: indexPath.row) // No need to adjust indexPath, since it forwards change directly to table view.
|
||||
shortcuts.remove(at: adjustedIndexPath.row)
|
||||
self.shortcutsDataSource.setItems(shortcuts, with: [deletion])
|
||||
|
||||
case .insert:
|
||||
|
||||
@ -12,8 +12,16 @@ import DeltaCore
|
||||
|
||||
import Roxas
|
||||
|
||||
protocol ControllerSkinsViewControllerDelegate: AnyObject
|
||||
{
|
||||
func controllerSkinsViewController(_ controllerSkinsViewController: ControllerSkinsViewController, didChooseControllerSkin controllerSkin: ControllerSkin)
|
||||
func controllerSkinsViewControllerDidResetControllerSkin(_ controllerSkinsViewController: ControllerSkinsViewController)
|
||||
}
|
||||
|
||||
class ControllerSkinsViewController: UITableViewController
|
||||
{
|
||||
weak var delegate: ControllerSkinsViewControllerDelegate?
|
||||
|
||||
var system: System! {
|
||||
didSet {
|
||||
self.updateDataSource()
|
||||
@ -26,8 +34,12 @@ class ControllerSkinsViewController: UITableViewController
|
||||
}
|
||||
}
|
||||
|
||||
var isResetButtonVisible: Bool = true
|
||||
|
||||
private let dataSource: RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>
|
||||
|
||||
@IBOutlet private var importControllerSkinButton: UIBarButtonItem!
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
self.dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||
@ -46,6 +58,13 @@ extension ControllerSkinsViewController
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.importControllerSkinButton.accessibilityLabel = NSLocalizedString("Import Controller Skin", comment: "")
|
||||
|
||||
if !self.isResetButtonVisible
|
||||
{
|
||||
self.navigationItem.rightBarButtonItems = [self.importControllerSkinButton]
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning()
|
||||
@ -114,6 +133,23 @@ private extension ControllerSkinsViewController
|
||||
|
||||
self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(ControllerSkin.name), cacheName: nil)
|
||||
}
|
||||
|
||||
@IBAction func resetControllerSkin(_ sender: UIBarButtonItem)
|
||||
{
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset Controller Skin to Default", comment: ""), style: .destructive, handler: { (action) in
|
||||
self.delegate?.controllerSkinsViewControllerDidResetControllerSkin(self)
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction private func importControllerSkin()
|
||||
{
|
||||
let importController = ImportController(documentTypes: ["com.rileytestut.delta.skin"])
|
||||
importController.delegate = self
|
||||
self.present(importController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension ControllerSkinsViewController
|
||||
@ -155,9 +191,7 @@ extension ControllerSkinsViewController
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let controllerSkin = self.dataSource.item(at: indexPath)
|
||||
Settings.setPreferredControllerSkin(controllerSkin, for: self.system, traits: self.traits)
|
||||
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
self.delegate?.controllerSkinsViewController(self, didChooseControllerSkin: controllerSkin)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
@ -173,3 +207,41 @@ extension ControllerSkinsViewController
|
||||
return height
|
||||
}
|
||||
}
|
||||
|
||||
extension ControllerSkinsViewController: ImportControllerDelegate
|
||||
{
|
||||
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
|
||||
{
|
||||
for error in errors
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
|
||||
if let error = errors.first
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.transitionCoordinator?.animate(alongsideTransition: nil) { _ in
|
||||
// Wait until ImportController is dismissed before presenting alert.
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Import Controller Skin", comment: ""), error: error)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let controllerSkinURLs = urls.filter { $0.pathExtension.lowercased() == "deltaskin" }
|
||||
DatabaseManager.shared.importControllerSkins(at: Set(controllerSkinURLs)) { (controllerSkins, errors) in
|
||||
if errors.count > 0
|
||||
{
|
||||
let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
if controllerSkins.count > 0
|
||||
{
|
||||
print("Imported Controller Skins:", controllerSkins.map { $0.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// SystemControllerSkinsViewController.swift
|
||||
// PreferredControllerSkinsViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/30/16.
|
||||
@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
import DeltaCore
|
||||
|
||||
extension SystemControllerSkinsViewController
|
||||
extension PreferredControllerSkinsViewController
|
||||
{
|
||||
private enum Section: Int
|
||||
{
|
||||
@ -19,10 +19,17 @@ extension SystemControllerSkinsViewController
|
||||
}
|
||||
}
|
||||
|
||||
class SystemControllerSkinsViewController: UITableViewController
|
||||
class PreferredControllerSkinsViewController: UITableViewController
|
||||
{
|
||||
var system: System!
|
||||
|
||||
var game: Game? {
|
||||
didSet {
|
||||
guard let game = self.game, let system = System(gameType: game.type) else { return }
|
||||
self.system = system
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet private var portraitImageView: UIImageView!
|
||||
@IBOutlet private var landscapeImageView: UIImageView!
|
||||
|
||||
@ -31,13 +38,19 @@ class SystemControllerSkinsViewController: UITableViewController
|
||||
private var landscapeControllerSkin: ControllerSkin?
|
||||
}
|
||||
|
||||
extension SystemControllerSkinsViewController
|
||||
extension PreferredControllerSkinsViewController
|
||||
{
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.title = self.system.localizedShortName
|
||||
self.title = self.game?.name ?? self.system.localizedShortName
|
||||
|
||||
if self.navigationController?.viewControllers.first != self
|
||||
{
|
||||
// Hide Done button since we are not root view controller.
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
@ -68,6 +81,7 @@ extension SystemControllerSkinsViewController
|
||||
guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell), let window = self.view.window else { return }
|
||||
|
||||
let controllerSkinsViewController = segue.destination as! ControllerSkinsViewController
|
||||
controllerSkinsViewController.delegate = self
|
||||
controllerSkinsViewController.system = self.system
|
||||
|
||||
var traits = DeltaCore.ControllerSkin.Traits.defaults(for: window)
|
||||
@ -80,10 +94,31 @@ extension SystemControllerSkinsViewController
|
||||
}
|
||||
|
||||
controllerSkinsViewController.traits = traits
|
||||
|
||||
let isResetButtonVisible: Bool
|
||||
|
||||
if let game = self.game
|
||||
{
|
||||
switch section
|
||||
{
|
||||
case .portrait: isResetButtonVisible = (game.preferredPortraitSkin != nil)
|
||||
case .landscape: isResetButtonVisible = (game.preferredLandscapeSkin != nil)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch section
|
||||
{
|
||||
case .portrait: isResetButtonVisible = !(self.portraitControllerSkin?.isStandard ?? false)
|
||||
case .landscape: isResetButtonVisible = !(self.portraitControllerSkin?.isStandard ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
controllerSkinsViewController.isResetButtonVisible = isResetButtonVisible
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemControllerSkinsViewController
|
||||
extension PreferredControllerSkinsViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
@ -115,7 +150,7 @@ extension SystemControllerSkinsViewController
|
||||
}
|
||||
}
|
||||
|
||||
private extension SystemControllerSkinsViewController
|
||||
private extension PreferredControllerSkinsViewController
|
||||
{
|
||||
func updateControllerSkins()
|
||||
{
|
||||
@ -126,7 +161,25 @@ private extension SystemControllerSkinsViewController
|
||||
let portraitTraits = self.makeTraits(orientation: .portrait, in: window)
|
||||
let landscapeTraits = self.makeTraits(orientation: .landscape, in: window)
|
||||
|
||||
let portraitControllerSkin = Settings.preferredControllerSkin(for: self.system, traits: portraitTraits)
|
||||
var portraitControllerSkin: ControllerSkin?
|
||||
var landscapeControllerSkin: ControllerSkin?
|
||||
|
||||
if let game = self.game
|
||||
{
|
||||
portraitControllerSkin = Settings.preferredControllerSkin(for: game, traits: portraitTraits)
|
||||
landscapeControllerSkin = Settings.preferredControllerSkin(for: game, traits: landscapeTraits)
|
||||
}
|
||||
|
||||
if portraitControllerSkin == nil
|
||||
{
|
||||
portraitControllerSkin = Settings.preferredControllerSkin(for: self.system, traits: portraitTraits)
|
||||
}
|
||||
|
||||
if landscapeControllerSkin == nil
|
||||
{
|
||||
landscapeControllerSkin = Settings.preferredControllerSkin(for: self.system, traits: landscapeTraits)
|
||||
}
|
||||
|
||||
if portraitControllerSkin != self.portraitControllerSkin
|
||||
{
|
||||
self.portraitImageView.image = nil
|
||||
@ -135,7 +188,6 @@ private extension SystemControllerSkinsViewController
|
||||
self.portraitControllerSkin = portraitControllerSkin
|
||||
}
|
||||
|
||||
let landscapeControllerSkin = Settings.preferredControllerSkin(for: self.system, traits: landscapeTraits)
|
||||
if landscapeControllerSkin != self.landscapeControllerSkin
|
||||
{
|
||||
self.landscapeImageView.image = nil
|
||||
@ -186,3 +238,34 @@ private extension SystemControllerSkinsViewController
|
||||
return traits
|
||||
}
|
||||
}
|
||||
|
||||
extension PreferredControllerSkinsViewController: ControllerSkinsViewControllerDelegate
|
||||
{
|
||||
func controllerSkinsViewController(_ controllerSkinsViewController: ControllerSkinsViewController, didChooseControllerSkin controllerSkin: ControllerSkin)
|
||||
{
|
||||
if let game = self.game
|
||||
{
|
||||
Settings.setPreferredControllerSkin(controllerSkin, for: game, traits: controllerSkinsViewController.traits)
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings.setPreferredControllerSkin(controllerSkin, for: self.system, traits: controllerSkinsViewController.traits)
|
||||
}
|
||||
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func controllerSkinsViewControllerDidResetControllerSkin(_ controllerSkinsViewController: ControllerSkinsViewController)
|
||||
{
|
||||
if let game = self.game
|
||||
{
|
||||
Settings.setPreferredControllerSkin(nil, for: game, traits: controllerSkinsViewController.traits)
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings.setPreferredControllerSkin(nil, for: self.system, traits: controllerSkinsViewController.traits)
|
||||
}
|
||||
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
@ -223,6 +223,8 @@ private extension ControllerInputsViewController
|
||||
case .fastForward:
|
||||
image = #imageLiteral(resourceName: "FastForward")
|
||||
text = NSLocalizedString("Fast Forward", comment: "")
|
||||
|
||||
case .toggleFastForward: continue
|
||||
}
|
||||
|
||||
let item = MenuItem(text: text, image: image) { [unowned self] (item) in
|
||||
@ -235,6 +237,8 @@ private extension ControllerInputsViewController
|
||||
|
||||
self.actionsMenuViewController.items = items
|
||||
self.actionsMenuViewController.isVibrancyEnabled = false
|
||||
|
||||
self.actionsMenuViewController.collectionView.backgroundColor = nil
|
||||
}
|
||||
|
||||
func prepareCallouts()
|
||||
@ -255,6 +259,8 @@ private extension ControllerInputsViewController
|
||||
{
|
||||
let calloutView = InputCalloutView()
|
||||
calloutView.delegate = self
|
||||
calloutView.permittedArrowDirection = .any
|
||||
calloutView.constrainedInsets = self.view.safeAreaInsets
|
||||
self.calloutViews[AnyInput(input)] = calloutView
|
||||
}
|
||||
|
||||
|
||||
@ -131,7 +131,12 @@ private extension ControllersSettingsViewController
|
||||
{
|
||||
cell.accessoryType = .none
|
||||
cell.detailTextLabel?.text = nil
|
||||
cell.textLabel?.textColor = .darkText
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
cell.textLabel?.textColor = .label
|
||||
} else {
|
||||
cell.textLabel?.textColor = .darkText
|
||||
}
|
||||
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
|
||||
522
Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift
Normal file
522
Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift
Normal file
@ -0,0 +1,522 @@
|
||||
//
|
||||
// MelonDSCoreSettingsViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/13/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import MobileCoreServices
|
||||
import CryptoKit
|
||||
|
||||
import DeltaCore
|
||||
import MelonDSDeltaCore
|
||||
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
import Roxas
|
||||
|
||||
private extension MelonDSCoreSettingsViewController
|
||||
{
|
||||
enum Section: Int
|
||||
{
|
||||
case general
|
||||
case dsBIOS
|
||||
case dsiBIOS
|
||||
case changeCore
|
||||
}
|
||||
|
||||
enum BIOSError: LocalizedError
|
||||
{
|
||||
case unknownSize(URL)
|
||||
case incorrectHash(URL, hash: String, expectedHash: String)
|
||||
case unsupportedHash(URL, hash: String)
|
||||
|
||||
@available(iOS 13, *)
|
||||
case incorrectSize(URL, size: Int, validSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>>)
|
||||
|
||||
private static let byteFormatter: ByteCountFormatter = {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.includesActualByteCount = true
|
||||
formatter.countStyle = .binary
|
||||
return formatter
|
||||
}()
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unknownSize(let fileURL):
|
||||
return String(format: NSLocalizedString("%@’s size could not be determined.", comment: ""), fileURL.lastPathComponent)
|
||||
|
||||
case .incorrectHash(let fileURL, let md5Hash, let expectedHash):
|
||||
return String(format: NSLocalizedString("%@‘s checksum does not match the expected checksum.\n\nChecksum:\n%@\n\nExpected:\n%@", comment: ""), fileURL.lastPathComponent, md5Hash, expectedHash)
|
||||
|
||||
case .unsupportedHash(let fileURL, let md5Hash):
|
||||
return String(format: NSLocalizedString("%@ is not compatible with this version of Delta.\n\nChecksum:\n%@", comment: ""), fileURL.lastPathComponent, md5Hash)
|
||||
|
||||
case .incorrectSize(let fileURL, let size, let validSizes):
|
||||
let actualSize = BIOSError.byteFormatter.string(fromByteCount: Int64(size))
|
||||
|
||||
if let range = validSizes.first, validSizes.count == 1
|
||||
{
|
||||
if range.lowerBound == range.upperBound
|
||||
{
|
||||
// Single value
|
||||
let expectedSize = BIOSError.byteFormatter.string(fromByteCount: Int64(range.lowerBound.converted(to: .bytes).value))
|
||||
return String(format: NSLocalizedString("%@ is %@, but expected size is %@.", comment: ""), fileURL.lastPathComponent, actualSize, expectedSize)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Range
|
||||
BIOSError.byteFormatter.includesActualByteCount = false
|
||||
defer { BIOSError.byteFormatter.includesActualByteCount = true }
|
||||
|
||||
let lowerBound = BIOSError.byteFormatter.string(fromByteCount: Int64(range.lowerBound.converted(to: .bytes).value))
|
||||
let upperBound = BIOSError.byteFormatter.string(fromByteCount: Int64(range.upperBound.converted(to: .bytes).value))
|
||||
return String(format: NSLocalizedString("%@ is %@, but expected size is between %@ and %@.", comment: ""), fileURL.lastPathComponent, actualSize, lowerBound, upperBound)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var description = String(format: NSLocalizedString("%@ is %@, but expected sizes are:", comment: ""), fileURL.lastPathComponent, actualSize) + "\n"
|
||||
|
||||
let sortedRanges = validSizes.sorted(by: { $0.lowerBound < $1.lowerBound })
|
||||
for range in sortedRanges
|
||||
{
|
||||
// Assume BIOS with multiple valid file sizes don't use (>1 count) ranges.
|
||||
description += "\n" + BIOSError.byteFormatter.string(fromByteCount: Int64(range.lowerBound.converted(to: .bytes).value))
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
return NSLocalizedString("Please choose a different BIOS file.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MelonDSCoreSettingsViewController: UITableViewController
|
||||
{
|
||||
private var importingBIOS: SystemBIOS?
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
if let navigationController = self.navigationController, navigationController.viewControllers.first != self
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MelonDSCoreSettingsViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
if let core = Delta.registeredCores[.ds]
|
||||
{
|
||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||
// Prepare database in case we changed/updated cores.
|
||||
DatabaseManager.shared.prepare(core, in: context)
|
||||
context.saveWithErrorLogging()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MelonDSCoreSettingsViewController
|
||||
{
|
||||
func isSectionHidden(_ section: Section) -> Bool
|
||||
{
|
||||
#if BETA
|
||||
let isBeta = true
|
||||
#else
|
||||
let isBeta = false
|
||||
#endif
|
||||
|
||||
switch section
|
||||
{
|
||||
case .dsBIOS where Settings.preferredCore(for: .ds) == DS.core:
|
||||
// Using DeSmuME core, which doesn't require BIOS.
|
||||
return true
|
||||
|
||||
case .dsiBIOS where Settings.preferredCore(for: .ds) == DS.core || !isBeta:
|
||||
// Using DeSmuME core, which doesn't require BIOS,
|
||||
// or using public Delta version, which doesn't support DSi (yet).
|
||||
return true
|
||||
|
||||
case .changeCore where !isBeta:
|
||||
// Using public Delta version, which only supports melonDS core.
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MelonDSCoreSettingsViewController
|
||||
{
|
||||
func openMetadataURL(for key: DeltaCoreMetadata.Key)
|
||||
{
|
||||
guard let metadata = Settings.preferredCore(for: .ds)?.metadata else { return }
|
||||
|
||||
let item = metadata[key]
|
||||
guard let url = item?.url else {
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let safariViewController = SFSafariViewController(url: url)
|
||||
safariViewController.preferredControlTintColor = .deltaPurple
|
||||
self.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func locate<BIOS: SystemBIOS>(_ bios: BIOS)
|
||||
{
|
||||
self.importingBIOS = bios
|
||||
|
||||
var supportedTypes = [kUTTypeItem as String, kUTTypeContent as String, "com.apple.macbinary-archive" /* System UTI for .bin */]
|
||||
|
||||
// Explicitly support files with .bin and .rom extensions.
|
||||
if let binTypes = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "bin" as CFString, nil)?.takeRetainedValue()
|
||||
{
|
||||
let types = (binTypes as NSArray).map { $0 as! String }
|
||||
supportedTypes.append(contentsOf: types)
|
||||
}
|
||||
|
||||
if let romTypes = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "rom" as CFString, nil)?.takeRetainedValue()
|
||||
{
|
||||
let types = (romTypes as NSArray).map { $0 as! String }
|
||||
supportedTypes.append(contentsOf: types)
|
||||
}
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
|
||||
documentPicker.delegate = self
|
||||
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
documentPicker.overrideUserInterfaceStyle = .dark
|
||||
}
|
||||
|
||||
self.present(documentPicker, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func changeCore()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Change Emulator Core", comment: ""), message: NSLocalizedString("Save states are not compatible between different emulator cores. Make sure to use in-game saves in order to keep using your save data.\n\nYour existing save states will not be deleted and will be available whenever you switch cores again.", comment: ""), preferredStyle: .actionSheet)
|
||||
|
||||
var desmumeActionTitle = DS.core.metadata?.name.value ?? DS.core.name
|
||||
var melonDSActionTitle = MelonDS.core.metadata?.name.value ?? MelonDS.core.name
|
||||
|
||||
if Settings.preferredCore(for: .ds) == DS.core
|
||||
{
|
||||
desmumeActionTitle += " ✓"
|
||||
}
|
||||
else
|
||||
{
|
||||
melonDSActionTitle += " ✓"
|
||||
}
|
||||
|
||||
alertController.addAction(UIAlertAction(title: desmumeActionTitle, style: .default, handler: { (action) in
|
||||
Settings.setPreferredCore(DS.core, for: .ds)
|
||||
self.tableView.reloadData()
|
||||
}))
|
||||
|
||||
alertController.addAction(UIAlertAction(title: melonDSActionTitle, style: .default, handler: { (action) in
|
||||
Settings.setPreferredCore(MelonDS.core, for: .ds)
|
||||
self.tableView.reloadData()
|
||||
}))
|
||||
alertController.addAction(.cancel)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func willEnterForeground(_ notification: Notification)
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension MelonDSCoreSettingsViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection sectionIndex: Int) -> Int
|
||||
{
|
||||
let section = Section(rawValue: sectionIndex)!
|
||||
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
return 0
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, numberOfRowsInSection: sectionIndex)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .general:
|
||||
let key = DeltaCoreMetadata.Key.allCases[indexPath.row]
|
||||
let item = Settings.preferredCore(for: .ds)?.metadata?[key]
|
||||
|
||||
cell.detailTextLabel?.text = item?.value ?? NSLocalizedString("-", comment: "")
|
||||
cell.detailTextLabel?.textColor = .gray
|
||||
|
||||
if item?.url != nil
|
||||
{
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.selectionStyle = .default
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.accessoryType = .none
|
||||
cell.selectionStyle = .none
|
||||
}
|
||||
|
||||
cell.contentView.isHidden = (item == nil)
|
||||
|
||||
case .dsBIOS:
|
||||
let bios = DSBIOS.allCases[indexPath.row]
|
||||
|
||||
if FileManager.default.fileExists(atPath: bios.fileURL.path)
|
||||
{
|
||||
cell.accessoryType = .checkmark
|
||||
cell.detailTextLabel?.text = nil
|
||||
cell.detailTextLabel?.textColor = .gray
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.detailTextLabel?.text = NSLocalizedString("Required", comment: "")
|
||||
cell.detailTextLabel?.textColor = .red
|
||||
}
|
||||
|
||||
cell.selectionStyle = .default
|
||||
|
||||
case .dsiBIOS:
|
||||
let bios = DSiBIOS.allCases[indexPath.row]
|
||||
|
||||
if FileManager.default.fileExists(atPath: bios.fileURL.path)
|
||||
{
|
||||
cell.accessoryType = .checkmark
|
||||
cell.detailTextLabel?.text = nil
|
||||
cell.detailTextLabel?.textColor = .gray
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.detailTextLabel?.text = NSLocalizedString("Required", comment: "")
|
||||
cell.detailTextLabel?.textColor = .red
|
||||
}
|
||||
|
||||
cell.selectionStyle = .default
|
||||
|
||||
case .changeCore: break
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||
{
|
||||
guard let core = Settings.preferredCore(for: .ds) else { return }
|
||||
|
||||
let key = DeltaCoreMetadata.Key.allCases[indexPath.row]
|
||||
let lastKey = DeltaCoreMetadata.Key.allCases.reversed().first { core.metadata?[$0] != nil }
|
||||
|
||||
if key == lastKey
|
||||
{
|
||||
// Hide separator for last visible row in case we've hidden additional rows.
|
||||
cell.separatorInset.left = 0
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.separatorInset.left = self.view.layoutMargins.left
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .general:
|
||||
let key = DeltaCoreMetadata.Key.allCases[indexPath.row]
|
||||
self.openMetadataURL(for: key)
|
||||
|
||||
case .dsBIOS:
|
||||
let bios = DSBIOS.allCases[indexPath.row]
|
||||
self.locate(bios)
|
||||
|
||||
case .dsiBIOS:
|
||||
let bios = DSiBIOS.allCases[indexPath.row]
|
||||
self.locate(bios)
|
||||
|
||||
case .changeCore:
|
||||
self.changeCore()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, titleForHeaderInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, titleForFooterInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .general:
|
||||
let key = DeltaCoreMetadata.Key.allCases[indexPath.row]
|
||||
guard Settings.preferredCore(for: .ds)?.metadata?[key] != nil else { return 0 }
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
return 1
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, heightForHeaderInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
return 1
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, heightForFooterInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MelonDSCoreSettingsViewController: UIDocumentPickerDelegate
|
||||
{
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
|
||||
{
|
||||
self.importingBIOS = nil
|
||||
self.tableView.reloadData() // Reloading index path causes cell to disappear...
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])
|
||||
{
|
||||
defer {
|
||||
self.importingBIOS = nil
|
||||
self.tableView.reloadData() // Reloading index path causes cell to disappear...
|
||||
}
|
||||
|
||||
guard let fileURL = urls.first, let bios = self.importingBIOS else { return }
|
||||
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
do
|
||||
{
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
// Validate file size first (since that's easiest for users to understand).
|
||||
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
|
||||
guard let fileSize = attributes[.size] as? Int else { throw BIOSError.unknownSize(fileURL) }
|
||||
|
||||
let measurement = Measurement<UnitInformationStorage>(value: Double(fileSize), unit: .bytes)
|
||||
guard bios.validFileSizes.contains(where: { $0.contains(measurement) }) else { throw BIOSError.incorrectSize(fileURL, size: fileSize, validSizes: bios.validFileSizes) }
|
||||
|
||||
if bios.expectedMD5Hash != nil || !bios.unsupportedMD5Hashes.isEmpty
|
||||
{
|
||||
// Only calculate hash if we need to.
|
||||
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
|
||||
let md5Hash = Insecure.MD5.hash(data: data)
|
||||
let hashString = md5Hash.compactMap { String(format: "%02x", $0) }.joined()
|
||||
|
||||
if let expectedMD5Hash = bios.expectedMD5Hash
|
||||
{
|
||||
guard hashString == expectedMD5Hash else { throw BIOSError.incorrectHash(fileURL, hash: hashString, expectedHash: expectedMD5Hash) }
|
||||
}
|
||||
|
||||
guard !bios.unsupportedMD5Hashes.contains(hashString) else { throw BIOSError.unsupportedHash(fileURL, hash: hashString) }
|
||||
}
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: fileURL, to: bios.fileURL, shouldReplace: true)
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let title = String(format: NSLocalizedString("Could not import %@.", comment: ""), bios.filename)
|
||||
|
||||
var message = error.localizedDescription
|
||||
if let recoverySuggestion = error.localizedRecoverySuggestion
|
||||
{
|
||||
message += "\n\n" + recoverySuggestion
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
145
Delta/Settings/Cores/SystemBIOS.swift
Normal file
145
Delta/Settings/Cores/SystemBIOS.swift
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// SystemBIOS.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/19/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import MelonDSDeltaCore
|
||||
|
||||
protocol SystemBIOS
|
||||
{
|
||||
var fileURL: URL { get }
|
||||
var filename: String { get }
|
||||
|
||||
var expectedMD5Hash: String? { get }
|
||||
var unsupportedMD5Hashes: Set<String> { get }
|
||||
|
||||
// RangeSet would be preferable, but it's not in Swift stdlib yet.
|
||||
@available(iOS 13, *)
|
||||
var validFileSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>> { get }
|
||||
}
|
||||
|
||||
extension SystemBIOS
|
||||
{
|
||||
var filename: String {
|
||||
return self.fileURL.lastPathComponent
|
||||
}
|
||||
|
||||
var expectedMD5Hash: String? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var unsupportedMD5Hashes: Set<String> {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
enum DSBIOS: SystemBIOS, CaseIterable
|
||||
{
|
||||
case bios7
|
||||
case bios9
|
||||
case firmware
|
||||
|
||||
var fileURL: URL {
|
||||
switch self
|
||||
{
|
||||
case .bios7: return MelonDSEmulatorBridge.shared.bios7URL
|
||||
case .bios9: return MelonDSEmulatorBridge.shared.bios9URL
|
||||
case .firmware: return MelonDSEmulatorBridge.shared.firmwareURL
|
||||
}
|
||||
}
|
||||
|
||||
var expectedMD5Hash: String? {
|
||||
switch self
|
||||
{
|
||||
case .bios7: return "df692a80a5b1bc90728bc3dfc76cd948"
|
||||
case .bios9: return "a392174eb3e572fed6447e956bde4b25"
|
||||
case .firmware: return nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
var validFileSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>> {
|
||||
// From http://melonds.kuribo64.net/faq.php
|
||||
switch self
|
||||
{
|
||||
case .bios7:
|
||||
// 16KB
|
||||
return Set([16].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
|
||||
case .bios9:
|
||||
// 4KB
|
||||
return Set([4].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
|
||||
case .firmware:
|
||||
// 256KB or 512KB
|
||||
// DSi/3DS 128KB firmwares technically work but aren't bootable, so we treat them as unsupported.
|
||||
return Set([256, 512].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DSiBIOS: SystemBIOS, CaseIterable
|
||||
{
|
||||
case bios7
|
||||
case bios9
|
||||
case firmware
|
||||
case nand
|
||||
|
||||
var fileURL: URL {
|
||||
switch self
|
||||
{
|
||||
case .bios7: return MelonDSEmulatorBridge.shared.dsiBIOS7URL
|
||||
case .bios9: return MelonDSEmulatorBridge.shared.dsiBIOS9URL
|
||||
case .firmware: return MelonDSEmulatorBridge.shared.dsiFirmwareURL
|
||||
case .nand: return MelonDSEmulatorBridge.shared.dsiNANDURL
|
||||
}
|
||||
}
|
||||
|
||||
var unsupportedMD5Hashes: Set<String> {
|
||||
switch self
|
||||
{
|
||||
case .bios7:
|
||||
return [
|
||||
"c8b9fe70f1ef5cab8e55540cd1c13dc8", // BIOSDSI7.ROM
|
||||
"3fbb3f39bd9a96e5d743f138bd4b9907", // BIOSDSI9.ROM
|
||||
"87b665fce118f76251271c3732532777", // bios9i.bin
|
||||
]
|
||||
|
||||
case .bios9:
|
||||
return [
|
||||
"c8b9fe70f1ef5cab8e55540cd1c13dc8", // BIOSDSI7.ROM
|
||||
"3fbb3f39bd9a96e5d743f138bd4b9907", // BIOSDSI9.ROM
|
||||
"559dae4ea78eb9d67702c56c1d791e81", // bios7i.bin
|
||||
]
|
||||
|
||||
case .firmware: return []
|
||||
case .nand: return []
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
var validFileSizes: Set<ClosedRange<Measurement<UnitInformationStorage>>> {
|
||||
// From http://melonds.kuribo64.net/faq.php
|
||||
switch self
|
||||
{
|
||||
case .bios7:
|
||||
// 64KB
|
||||
return Set([64].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
|
||||
|
||||
case .bios9:
|
||||
// 64KB
|
||||
return Set([64].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
|
||||
|
||||
case .firmware:
|
||||
// 128KB
|
||||
return Set([128].map { Measurement(value: $0, unit: .kibibytes) }.map { $0...$0 })
|
||||
|
||||
case .nand:
|
||||
// 200MB - 300MB
|
||||
return Set([200...300].map { Measurement(value: $0.lowerBound, unit: .mebibytes) ... Measurement(value: $0.upperBound, unit: .mebibytes) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import MelonDSDeltaCore
|
||||
|
||||
import Roxas
|
||||
|
||||
@ -25,6 +26,8 @@ extension Settings
|
||||
|
||||
case system
|
||||
case traits
|
||||
|
||||
case core
|
||||
}
|
||||
|
||||
enum Name: String
|
||||
@ -55,7 +58,9 @@ struct Settings
|
||||
#keyPath(UserDefaults.gameShortcutsMode): GameShortcutsMode.recent.rawValue,
|
||||
#keyPath(UserDefaults.isButtonHapticFeedbackEnabled): true,
|
||||
#keyPath(UserDefaults.isThumbstickHapticFeedbackEnabled): true,
|
||||
#keyPath(UserDefaults.sortSaveStatesByOldestFirst): true] as [String : Any]
|
||||
#keyPath(UserDefaults.sortSaveStatesByOldestFirst): true,
|
||||
#keyPath(UserDefaults.isPreviewsEnabled): true,
|
||||
Settings.preferredCoreSettingsKey(for: .ds): MelonDS.core.identifier] as [String : Any]
|
||||
UserDefaults.standard.register(defaults: defaults)
|
||||
}
|
||||
}
|
||||
@ -177,6 +182,34 @@ extension Settings
|
||||
}
|
||||
}
|
||||
|
||||
static var isPreviewsEnabled: Bool {
|
||||
set { UserDefaults.standard.isPreviewsEnabled = newValue }
|
||||
get {
|
||||
let isPreviewsEnabled = UserDefaults.standard.isPreviewsEnabled
|
||||
return isPreviewsEnabled
|
||||
}
|
||||
}
|
||||
|
||||
static func preferredCore(for gameType: GameType) -> DeltaCoreProtocol?
|
||||
{
|
||||
let key = self.preferredCoreSettingsKey(for: gameType)
|
||||
|
||||
let identifier = UserDefaults.standard.string(forKey: key)
|
||||
|
||||
let core = System.allCores.first { $0.identifier == identifier }
|
||||
return core
|
||||
}
|
||||
|
||||
static func setPreferredCore(_ core: DeltaCoreProtocol, for gameType: GameType)
|
||||
{
|
||||
Delta.register(core)
|
||||
|
||||
let key = self.preferredCoreSettingsKey(for: gameType)
|
||||
|
||||
UserDefaults.standard.set(core.identifier, forKey: key)
|
||||
NotificationCenter.default.post(name: .settingsDidChange, object: nil, userInfo: [NotificationUserInfoKey.name: key, NotificationUserInfoKey.core: core])
|
||||
}
|
||||
|
||||
static func preferredControllerSkin(for system: System, traits: DeltaCore.ControllerSkin.Traits) -> ControllerSkin?
|
||||
{
|
||||
guard let userDefaultsKey = self.preferredControllerSkinKey(for: system, traits: traits) else { return nil }
|
||||
@ -217,16 +250,84 @@ extension Settings
|
||||
return nil
|
||||
}
|
||||
|
||||
static func setPreferredControllerSkin(_ controllerSkin: ControllerSkin, for system: System, traits: DeltaCore.ControllerSkin.Traits)
|
||||
static func setPreferredControllerSkin(_ controllerSkin: ControllerSkin?, for system: System, traits: DeltaCore.ControllerSkin.Traits)
|
||||
{
|
||||
guard let userDefaultKey = self.preferredControllerSkinKey(for: system, traits: traits) else { return }
|
||||
|
||||
guard UserDefaults.standard.string(forKey: userDefaultKey) != controllerSkin.identifier else { return }
|
||||
guard UserDefaults.standard.string(forKey: userDefaultKey) != controllerSkin?.identifier else { return }
|
||||
|
||||
UserDefaults.standard.set(controllerSkin.identifier, forKey: userDefaultKey)
|
||||
UserDefaults.standard.set(controllerSkin?.identifier, forKey: userDefaultKey)
|
||||
|
||||
NotificationCenter.default.post(name: .settingsDidChange, object: controllerSkin, userInfo: [NotificationUserInfoKey.name: Name.preferredControllerSkin, NotificationUserInfoKey.system: system, NotificationUserInfoKey.traits: traits])
|
||||
}
|
||||
|
||||
static func preferredControllerSkin(for game: Game, traits: DeltaCore.ControllerSkin.Traits) -> ControllerSkin?
|
||||
{
|
||||
let preferredControllerSkin: ControllerSkin?
|
||||
|
||||
switch traits.orientation
|
||||
{
|
||||
case .portrait: preferredControllerSkin = game.preferredPortraitSkin
|
||||
case .landscape: preferredControllerSkin = game.preferredLandscapeSkin
|
||||
}
|
||||
|
||||
if let controllerSkin = preferredControllerSkin, let _ = controllerSkin.supportedTraits(for: traits)
|
||||
{
|
||||
// Check if there are supported traits, which includes fallback traits for X <-> non-X devices.
|
||||
return controllerSkin
|
||||
}
|
||||
|
||||
if let system = System(gameType: game.type)
|
||||
{
|
||||
// Fall back to using preferred controller skin for the system.
|
||||
let controllerSkin = Settings.preferredControllerSkin(for: system, traits: traits)
|
||||
return controllerSkin
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func setPreferredControllerSkin(_ controllerSkin: ControllerSkin?, for game: Game, traits: DeltaCore.ControllerSkin.Traits)
|
||||
{
|
||||
let context = DatabaseManager.shared.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
let game = context.object(with: game.objectID) as! Game
|
||||
|
||||
let skin: ControllerSkin?
|
||||
if let controllerSkin = controllerSkin, let contextSkin = context.object(with: controllerSkin.objectID) as? ControllerSkin
|
||||
{
|
||||
skin = contextSkin
|
||||
}
|
||||
else
|
||||
{
|
||||
skin = nil
|
||||
}
|
||||
|
||||
switch traits.orientation
|
||||
{
|
||||
case .portrait: game.preferredPortraitSkin = skin
|
||||
case .landscape: game.preferredLandscapeSkin = skin
|
||||
}
|
||||
|
||||
context.saveWithErrorLogging()
|
||||
}
|
||||
|
||||
game.managedObjectContext?.refresh(game, mergeChanges: false)
|
||||
|
||||
if let system = System(gameType: game.type)
|
||||
{
|
||||
NotificationCenter.default.post(name: .settingsDidChange, object: controllerSkin, userInfo: [NotificationUserInfoKey.name: Name.preferredControllerSkin, NotificationUserInfoKey.system: system, NotificationUserInfoKey.traits: traits])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Settings
|
||||
{
|
||||
static func preferredCoreSettingsKey(for gameType: GameType) -> String
|
||||
{
|
||||
let key = "core." + gameType.rawValue
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
private extension Settings
|
||||
@ -243,6 +344,7 @@ private extension Settings
|
||||
case .gba: systemName = "gba"
|
||||
case .n64: systemName = "n64"
|
||||
case .ds: systemName = "ds"
|
||||
case .genesis: systemName = "genesis"
|
||||
}
|
||||
|
||||
let orientation: String
|
||||
@ -281,4 +383,6 @@ private extension UserDefaults
|
||||
@NSManaged var isThumbstickHapticFeedbackEnabled: Bool
|
||||
|
||||
@NSManaged var sortSaveStatesByOldestFirst: Bool
|
||||
|
||||
@NSManaged var isPreviewsEnabled: Bool
|
||||
}
|
||||
|
||||
@ -22,7 +22,8 @@ private extension SettingsViewController
|
||||
case controllerOpacity
|
||||
case hapticFeedback
|
||||
case syncing
|
||||
case threeDTouch
|
||||
case hapticTouch
|
||||
case cores
|
||||
case patreon
|
||||
case credits
|
||||
}
|
||||
@ -31,6 +32,7 @@ private extension SettingsViewController
|
||||
{
|
||||
case controllers = "controllersSegue"
|
||||
case controllerSkins = "controllerSkinsSegue"
|
||||
case dsSettings = "dsSettingsSegue"
|
||||
}
|
||||
|
||||
enum SyncingRow: Int, CaseIterable
|
||||
@ -44,6 +46,7 @@ private extension SettingsViewController
|
||||
case riley
|
||||
case caroline
|
||||
case grant
|
||||
case litRitt
|
||||
case softwareLicenses
|
||||
}
|
||||
}
|
||||
@ -55,6 +58,7 @@ class SettingsViewController: UITableViewController
|
||||
|
||||
@IBOutlet private var buttonHapticFeedbackEnabledSwitch: UISwitch!
|
||||
@IBOutlet private var thumbstickHapticFeedbackEnabledSwitch: UISwitch!
|
||||
@IBOutlet private var previewsEnabledSwitch: UISwitch!
|
||||
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
|
||||
@ -139,10 +143,12 @@ class SettingsViewController: UITableViewController
|
||||
controllersSettingsViewController.playerIndex = indexPath.row
|
||||
|
||||
case Segue.controllerSkins:
|
||||
let systemControllerSkinsViewController = segue.destination as! SystemControllerSkinsViewController
|
||||
let preferredControllerSkinsViewController = segue.destination as! PreferredControllerSkinsViewController
|
||||
|
||||
let system = System.registeredSystems[indexPath.row]
|
||||
systemControllerSkinsViewController.system = system
|
||||
preferredControllerSkinsViewController.system = system
|
||||
|
||||
case Segue.dsSettings: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -168,6 +174,7 @@ private extension SettingsViewController
|
||||
|
||||
self.buttonHapticFeedbackEnabledSwitch.isOn = Settings.isButtonHapticFeedbackEnabled
|
||||
self.thumbstickHapticFeedbackEnabledSwitch.isOn = Settings.isThumbstickHapticFeedbackEnabled
|
||||
self.previewsEnabledSwitch.isOn = Settings.isPreviewsEnabled
|
||||
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
@ -182,7 +189,17 @@ private extension SettingsViewController
|
||||
{
|
||||
switch section
|
||||
{
|
||||
case .threeDTouch: return self.view.traitCollection.forceTouchCapability != .available
|
||||
case .hapticTouch:
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
// All devices on iOS 13 support either 3D touch or Haptic Touch.
|
||||
return false
|
||||
}
|
||||
else
|
||||
{
|
||||
return self.view.traitCollection.forceTouchCapability != .available
|
||||
}
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@ -226,6 +243,11 @@ private extension SettingsViewController
|
||||
Settings.isThumbstickHapticFeedbackEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@IBAction func togglePreviewsEnabled(_ sender: UISwitch)
|
||||
{
|
||||
Settings.isPreviewsEnabled = sender.isOn
|
||||
}
|
||||
|
||||
func openTwitter(username: String)
|
||||
{
|
||||
let twitterAppURL = URL(string: "twitter://user?screen_name=" + username)!
|
||||
@ -341,7 +363,11 @@ extension SettingsViewController
|
||||
case .service: break
|
||||
}
|
||||
|
||||
case .controllerOpacity, .hapticFeedback, .threeDTouch, .patreon, .credits: break
|
||||
case .cores:
|
||||
let preferredCore = Settings.preferredCore(for: .ds)
|
||||
cell.detailTextLabel?.text = preferredCore?.metadata?.name.value ?? preferredCore?.name ?? NSLocalizedString("Unknown", comment: "")
|
||||
|
||||
case .controllerOpacity, .hapticFeedback, .hapticTouch, .patreon, .credits: break
|
||||
}
|
||||
|
||||
return cell
|
||||
@ -356,7 +382,8 @@ extension SettingsViewController
|
||||
{
|
||||
case .controllers: self.performSegue(withIdentifier: Segue.controllers.rawValue, sender: cell)
|
||||
case .controllerSkins: self.performSegue(withIdentifier: Segue.controllerSkins.rawValue, sender: cell)
|
||||
case .controllerOpacity, .hapticFeedback, .threeDTouch, .syncing: break
|
||||
case .cores: self.performSegue(withIdentifier: Segue.dsSettings.rawValue, sender: cell)
|
||||
case .controllerOpacity, .hapticFeedback, .hapticTouch, .syncing: break
|
||||
case .patreon:
|
||||
let patreonURL = URL(string: "altstore://patreon")!
|
||||
|
||||
@ -379,26 +406,35 @@ extension SettingsViewController
|
||||
case .riley: self.openTwitter(username: "rileytestut")
|
||||
case .caroline: self.openTwitter(username: "1carolinemoore")
|
||||
case .grant: self.openTwitter(username: "grantgliner")
|
||||
case .litRitt: self.openTwitter(username: "litritt_z")
|
||||
case .softwareLicenses: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
#if !BETA
|
||||
case .credits where indexPath.row == CreditsRow.litRitt.rawValue: return 0.0
|
||||
#endif
|
||||
default: return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return nil }
|
||||
|
||||
if isSectionHidden(section)
|
||||
switch section
|
||||
{
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, titleForHeaderInSection: section.rawValue)
|
||||
case .hapticTouch where self.view.traitCollection.forceTouchCapability == .available: return NSLocalizedString("3D Touch", comment: "")
|
||||
default: return super.tableView(tableView, titleForHeaderInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
|
||||
@ -77,11 +77,19 @@ private extension GameSyncStatusViewController
|
||||
let configure = { [weak self] (cell: UITableViewCell, recordedObject: NSManagedObject) in
|
||||
if let record = self?.recordsByObjectURI[recordedObject.objectID.uriRepresentation()], record.isConflicted
|
||||
{
|
||||
cell.textLabel?.textColor = .red
|
||||
if #available(iOS 13.0, *) {
|
||||
cell.textLabel?.textColor = .systemRed
|
||||
} else {
|
||||
cell.textLabel?.textColor = .red
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.textLabel?.textColor = .darkText
|
||||
if #available(iOS 13.0, *) {
|
||||
cell.textLabel?.textColor = .label
|
||||
} else {
|
||||
cell.textLabel?.textColor = .darkText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
import UIKit
|
||||
|
||||
import Harmony
|
||||
import Harmony_Drive
|
||||
|
||||
import Roxas
|
||||
|
||||
|
||||
@ -116,6 +116,20 @@
|
||||
<string>com.rileytestut.delta.game.ds</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Genesis Game</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.rileytestut.delta.game.genesis</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
@ -185,21 +199,8 @@
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>gamefaqs.net</key>
|
||||
<dict>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSTemporaryExceptionMinimumTLSVersion</key>
|
||||
<string>TLSv1.1</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Delta uses your microphone to emulate the Nintendo DS microphone.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Press "OK" to allow Delta to use images from your Photo Library as game artwork.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
@ -222,8 +223,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
@ -276,7 +277,9 @@
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<string>gba</string>
|
||||
<array>
|
||||
<string>gba</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
@ -287,12 +290,16 @@
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Delta Controller Skin</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.rileytestut.delta.skin</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<string>deltaskin</string>
|
||||
<array>
|
||||
<string>deltaskin</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
@ -370,6 +377,28 @@
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>com.rileytestut.delta.game</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Genesis Game</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.rileytestut.delta.game.genesis</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>gen</string>
|
||||
<string>bin</string>
|
||||
<string>md</string>
|
||||
<string>smd</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -7,8 +7,6 @@
|
||||
//
|
||||
|
||||
import Harmony
|
||||
import Harmony_Drive
|
||||
import Harmony_Dropbox
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
|
||||
@ -163,6 +163,16 @@ private extension SyncResultViewController
|
||||
errorMessage = messages.joined(separator: "\n")
|
||||
}
|
||||
|
||||
case .other(_, ValidationError.nilRelationshipObjects(let relationships)) where relationships.contains("game"):
|
||||
if let gameName = error.record.localMetadata?[.gameName] ?? error.record.remoteMetadata?[.gameName]
|
||||
{
|
||||
errorMessage = String(format: NSLocalizedString("“%@“ is missing. Please re-import this game to resume syncing its data.", comment: ""), gameName)
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = NSLocalizedString("The game for this item is missing. Please re-import the game to resume syncing its data.", comment: "")
|
||||
}
|
||||
|
||||
case .other(_, let error as NSError): errorMessage = error.localizedFailureReason ?? error.localizedDescription
|
||||
default: errorMessage = error.failureReason
|
||||
}
|
||||
@ -265,12 +275,38 @@ private extension SyncResultViewController
|
||||
|
||||
for (group, errors) in errorsByGroup
|
||||
{
|
||||
let sortedErrors = errors.sorted { (a, b) -> Bool in
|
||||
let filteredErrors = errors.filter { error in
|
||||
switch group
|
||||
{
|
||||
case .saveState(let gameID), .cheat(let gameID):
|
||||
switch error
|
||||
{
|
||||
case RecordError.other(_, ValidationError.nilRelationshipObjects(let relationships)) where relationships.contains("game"):
|
||||
if errorsByGroup.keys.contains(Group.game(gameID))
|
||||
{
|
||||
// There is already an error for this game, so don't need to duplicate it due to it missing.
|
||||
return false
|
||||
}
|
||||
else
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
let sortedErrors = filteredErrors.sorted { (a, b) -> Bool in
|
||||
guard let a = a as? RecordError, let b = b as? RecordError else { return false }
|
||||
return a.record.localizedTitle < b.record.localizedTitle
|
||||
}
|
||||
|
||||
errorsByGroup[group] = sortedErrors
|
||||
errorsByGroup[group] = sortedErrors.isEmpty ? nil : sortedErrors
|
||||
}
|
||||
|
||||
let sortedErrors = errorsByGroup.sorted { (a, b) in
|
||||
|
||||
99
Delta/Systems/DeltaCoreProtocol+Delta.swift
Normal file
99
Delta/Systems/DeltaCoreProtocol+Delta.swift
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// DeltaCoreProtocol+Delta.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/30/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import DeltaCore
|
||||
|
||||
import NESDeltaCore
|
||||
import SNESDeltaCore
|
||||
import GBCDeltaCore
|
||||
import GBADeltaCore
|
||||
import N64DeltaCore
|
||||
import MelonDSDeltaCore
|
||||
|
||||
import Systems
|
||||
|
||||
// Legacy Cores
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
@dynamicMemberLookup
|
||||
struct DeltaCoreMetadata
|
||||
{
|
||||
enum Key: CaseIterable
|
||||
{
|
||||
case name
|
||||
case developer
|
||||
case source
|
||||
case donate
|
||||
}
|
||||
|
||||
struct Item
|
||||
{
|
||||
var value: String
|
||||
var url: URL?
|
||||
}
|
||||
|
||||
var name: Item { self.items[.name]! }
|
||||
private let items: [Key: Item]
|
||||
|
||||
init?(_ items: [Key: Item])
|
||||
{
|
||||
guard items.keys.contains(.name) else { return nil }
|
||||
self.items = items
|
||||
}
|
||||
|
||||
subscript(dynamicMember keyPath: KeyPath<Key.Type, Key>) -> Item?
|
||||
{
|
||||
let key = Key.self[keyPath: keyPath]
|
||||
return self[key]
|
||||
}
|
||||
|
||||
subscript(_ key: Key) -> Item?
|
||||
{
|
||||
let item = self.items[key]
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
extension DeltaCoreProtocol
|
||||
{
|
||||
var supportedRates: ClosedRange<Double> {
|
||||
return 1...self.maximumFastForwardSpeed
|
||||
}
|
||||
|
||||
private var maximumFastForwardSpeed: Double {
|
||||
switch self
|
||||
{
|
||||
case NES.core, SNES.core, GBC.core: return 4
|
||||
case GBA.core: return 3
|
||||
case N64.core where UIDevice.current.hasA11ProcessorOrBetter: return 3
|
||||
case N64.core where UIDevice.current.hasA9ProcessorOrBetter: return 1.5
|
||||
case MelonDS.core where UIDevice.current.supportsJIT: return 3
|
||||
case MelonDS.core where UIDevice.current.hasA11ProcessorOrBetter: return 1.5
|
||||
case GPGX.core: return 4
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
|
||||
var metadata: DeltaCoreMetadata? {
|
||||
switch self
|
||||
{
|
||||
case DS.core:
|
||||
return DeltaCoreMetadata([.name: .init(value: NSLocalizedString("DeSmuME (Legacy)", comment: ""), url: URL(string: "http://desmume.org")),
|
||||
.developer: .init(value: NSLocalizedString("DeSmuME team", comment: ""), url: URL(string: "https://wiki.desmume.org/index.php?title=DeSmuME:About")),
|
||||
.source: .init(value: NSLocalizedString("GitHub", comment: ""), url: URL(string: "https://github.com/TASVideos/desmume"))])
|
||||
|
||||
case MelonDS.core:
|
||||
return DeltaCoreMetadata([.name: .init(value: NSLocalizedString("melonDS", comment: ""), url: URL(string: "http://melonds.kuribo64.net")),
|
||||
.developer: .init(value: NSLocalizedString("Arisotura", comment: ""), url: URL(string: "https://twitter.com/Arisotura")),
|
||||
.source: .init(value: NSLocalizedString("GitHub", comment: ""), url: URL(string: "https://github.com/Arisotura/melonDS")),
|
||||
.donate: .init(value: NSLocalizedString("Patreon", comment: ""), url: URL(string: "https://www.patreon.com/staplebutter"))])
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,11 +13,16 @@ import GBADeltaCore
|
||||
import GBCDeltaCore
|
||||
import NESDeltaCore
|
||||
import N64DeltaCore
|
||||
import DSDeltaCore
|
||||
import MelonDSDeltaCore
|
||||
import Systems
|
||||
|
||||
// Legacy Cores
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
enum System: CaseIterable
|
||||
{
|
||||
case nes
|
||||
case genesis
|
||||
case snes
|
||||
case n64
|
||||
case gbc
|
||||
@ -28,6 +33,10 @@ enum System: CaseIterable
|
||||
let systems = System.allCases.filter { Delta.registeredCores.keys.contains($0.gameType) }
|
||||
return systems
|
||||
}
|
||||
|
||||
static var allCores: [DeltaCoreProtocol] {
|
||||
return [NES.core, SNES.core, N64.core, GBC.core, GBA.core, DS.core, MelonDS.core, GPGX.core]
|
||||
}
|
||||
}
|
||||
|
||||
extension System
|
||||
@ -41,6 +50,7 @@ extension System
|
||||
case .gbc: return NSLocalizedString("Game Boy Color", comment: "")
|
||||
case .gba: return NSLocalizedString("Game Boy Advance", comment: "")
|
||||
case .ds: return NSLocalizedString("Nintendo DS", comment: "")
|
||||
case .genesis: return NSLocalizedString("Sega Genesis", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +62,8 @@ extension System
|
||||
case .n64: return NSLocalizedString("N64", comment: "")
|
||||
case .gbc: return NSLocalizedString("GBC", comment: "")
|
||||
case .gba: return NSLocalizedString("GBA", comment: "")
|
||||
case .ds: return NSLocalizedString("DS (Beta)", comment: "")
|
||||
case .ds: return NSLocalizedString("DS", comment: "")
|
||||
case .genesis: return NSLocalizedString("Genesis (Beta)", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,6 +71,7 @@ extension System
|
||||
switch self
|
||||
{
|
||||
case .nes: return 1985
|
||||
case .genesis: return 1989
|
||||
case .snes: return 1990
|
||||
case .n64: return 1996
|
||||
case .gbc: return 1998
|
||||
@ -79,11 +91,12 @@ extension System
|
||||
case .n64: return N64.core
|
||||
case .gbc: return GBC.core
|
||||
case .gba: return GBA.core
|
||||
case .ds: return DS.core
|
||||
case .ds: return Settings.preferredCore(for: .ds) ?? MelonDS.core
|
||||
case .genesis: return GPGX.core
|
||||
}
|
||||
}
|
||||
|
||||
var gameType: GameType {
|
||||
var gameType: DeltaCore.GameType {
|
||||
switch self
|
||||
{
|
||||
case .nes: return .nes
|
||||
@ -92,10 +105,11 @@ extension System
|
||||
case .gbc: return .gbc
|
||||
case .gba: return .gba
|
||||
case .ds: return .ds
|
||||
case .genesis: return .genesis
|
||||
}
|
||||
}
|
||||
|
||||
init?(gameType: GameType)
|
||||
init?(gameType: DeltaCore.GameType)
|
||||
{
|
||||
switch gameType
|
||||
{
|
||||
@ -105,12 +119,13 @@ extension System
|
||||
case GameType.gbc: self = .gbc
|
||||
case GameType.gba: self = .gba
|
||||
case GameType.ds: self = .ds
|
||||
case GameType.genesis: self = .genesis
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GameType
|
||||
extension DeltaCore.GameType
|
||||
{
|
||||
init?(fileExtension: String)
|
||||
{
|
||||
@ -122,6 +137,7 @@ extension GameType
|
||||
case "gbc", "gb": self = .gbc
|
||||
case "gba": self = .gba
|
||||
case "ds", "nds": self = .ds
|
||||
case "gen", "bin", "md", "smd": self = .genesis
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
2
External/Harmony
vendored
2
External/Harmony
vendored
@ -1 +1 @@
|
||||
Subproject commit 4d7ba40073c8fed53fa92f7db07d75b4b8929a41
|
||||
Subproject commit db5fbd829ac5aa6e7e249eb9fabf07bbb75fabd9
|
||||
2
External/Roxas
vendored
2
External/Roxas
vendored
@ -1 +1 @@
|
||||
Subproject commit 89510d73ca548da6094348bc2c7b7f1887475d55
|
||||
Subproject commit 2bb3182495f680ce60da8e72c3d84a7d4451ef75
|
||||
35
Podfile
35
Podfile
@ -1,12 +1,39 @@
|
||||
platform :ios, '10.0'
|
||||
platform :ios, '12.0'
|
||||
|
||||
use_frameworks!
|
||||
inhibit_all_warnings!
|
||||
|
||||
target 'Delta' do
|
||||
pod 'SQLite.swift', '~> 0.11.0'
|
||||
use_modular_headers!
|
||||
|
||||
pod 'SQLite.swift', '~> 0.12.0'
|
||||
pod 'SDWebImage', '~> 3.8'
|
||||
pod 'Fabric', '~> 1.6.0'
|
||||
pod 'Crashlytics', '~> 3.8.0'
|
||||
pod 'SMCalloutView'
|
||||
pod 'SMCalloutView', '~> 2.1.0'
|
||||
|
||||
pod 'DeltaCore', :path => 'Cores/DeltaCore'
|
||||
pod 'NESDeltaCore', :path => 'Cores/NESDeltaCore'
|
||||
pod 'SNESDeltaCore', :path => 'Cores/SNESDeltaCore'
|
||||
pod 'N64DeltaCore', :path => 'Cores/N64DeltaCore'
|
||||
pod 'GBCDeltaCore', :path => 'Cores/GBCDeltaCore'
|
||||
pod 'GBADeltaCore', :path => 'Cores/GBADeltaCore'
|
||||
pod 'DSDeltaCore', :path => 'Cores/DSDeltaCore'
|
||||
pod 'MelonDSDeltaCore', :path => 'Cores/MelonDSDeltaCore'
|
||||
pod 'Roxas', :path => 'External/Roxas'
|
||||
pod 'Harmony', :path => 'External/Harmony'
|
||||
end
|
||||
|
||||
# Unlink DeltaCore to prevent conflicts with Systems.framework
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
if target.name == "Pods-Delta"
|
||||
puts "Updating #{target.name} OTHER_LDFLAGS"
|
||||
target.build_configurations.each do |config|
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
xcconfig = File.read(xcconfig_path)
|
||||
new_xcconfig = xcconfig.sub('-l"DeltaCore"', '')
|
||||
File.open(xcconfig_path, "w") { |file| file << new_xcconfig }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
138
Podfile.lock
138
Podfile.lock
@ -1,37 +1,153 @@
|
||||
PODS:
|
||||
- Alamofire (4.7.3)
|
||||
- Crashlytics (3.8.6):
|
||||
- Fabric (~> 1.6.3)
|
||||
- DeltaCore (0.1):
|
||||
- ZIPFoundation
|
||||
- DSDeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- Fabric (1.6.13)
|
||||
- GBADeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- GBCDeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- GoogleAPIClientForREST/Core (1.3.11):
|
||||
- GTMSessionFetcher (>= 1.1.7)
|
||||
- GoogleAPIClientForREST/Drive (1.3.11):
|
||||
- GoogleAPIClientForREST/Core
|
||||
- GTMSessionFetcher (>= 1.1.7)
|
||||
- GoogleSignIn (4.4.0):
|
||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)"
|
||||
- GTMSessionFetcher/Core (~> 1.1)
|
||||
- GoogleToolboxForMac/DebugUtils (2.3.0):
|
||||
- GoogleToolboxForMac/Defines (= 2.3.0)
|
||||
- GoogleToolboxForMac/Defines (2.3.0)
|
||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.0)":
|
||||
- GoogleToolboxForMac/DebugUtils (= 2.3.0)
|
||||
- GoogleToolboxForMac/Defines (= 2.3.0)
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (= 2.3.0)"
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (2.3.0)"
|
||||
- GTMSessionFetcher (1.5.0):
|
||||
- GTMSessionFetcher/Full (= 1.5.0)
|
||||
- GTMSessionFetcher/Core (1.5.0)
|
||||
- GTMSessionFetcher/Full (1.5.0):
|
||||
- GTMSessionFetcher/Core (= 1.5.0)
|
||||
- Harmony (0.1):
|
||||
- Harmony/Harmony-Drive (= 0.1)
|
||||
- Harmony/Harmony-Dropbox (= 0.1)
|
||||
- Roxas
|
||||
- Harmony/Harmony-Drive (0.1):
|
||||
- GoogleAPIClientForREST/Drive (~> 1.3.0)
|
||||
- GoogleSignIn (~> 4.4.0)
|
||||
- Roxas
|
||||
- Harmony/Harmony-Dropbox (0.1):
|
||||
- Roxas
|
||||
- SwiftyDropbox (~> 5.0.0)
|
||||
- MelonDSDeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- N64DeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- N64DeltaCore/RSP (= 0.1)
|
||||
- N64DeltaCore/Video (= 0.1)
|
||||
- N64DeltaCore/RSP (0.1):
|
||||
- DeltaCore
|
||||
- N64DeltaCore/Video (0.1):
|
||||
- DeltaCore
|
||||
- NESDeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- Roxas (0.1)
|
||||
- SDWebImage (3.8.3):
|
||||
- SDWebImage/Core (= 3.8.3)
|
||||
- SDWebImage/Core (3.8.3)
|
||||
- SMCalloutView (2.1.5)
|
||||
- SQLite.swift (0.11.6):
|
||||
- SQLite.swift/standard (= 0.11.6)
|
||||
- SQLite.swift/standard (0.11.6)
|
||||
- SNESDeltaCore (0.1):
|
||||
- DeltaCore
|
||||
- SQLite.swift (0.12.2):
|
||||
- SQLite.swift/standard (= 0.12.2)
|
||||
- SQLite.swift/standard (0.12.2)
|
||||
- SwiftyDropbox (5.0.0):
|
||||
- Alamofire (~> 4.7.3)
|
||||
- ZIPFoundation (0.9.11)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Crashlytics (~> 3.8.0)
|
||||
- DeltaCore (from `Cores/DeltaCore`)
|
||||
- DSDeltaCore (from `Cores/DSDeltaCore`)
|
||||
- Fabric (~> 1.6.0)
|
||||
- GBADeltaCore (from `Cores/GBADeltaCore`)
|
||||
- GBCDeltaCore (from `Cores/GBCDeltaCore`)
|
||||
- Harmony (from `External/Harmony`)
|
||||
- MelonDSDeltaCore (from `Cores/MelonDSDeltaCore`)
|
||||
- N64DeltaCore (from `Cores/N64DeltaCore`)
|
||||
- NESDeltaCore (from `Cores/NESDeltaCore`)
|
||||
- Roxas (from `External/Roxas`)
|
||||
- SDWebImage (~> 3.8)
|
||||
- SMCalloutView
|
||||
- SQLite.swift (~> 0.11.0)
|
||||
- SMCalloutView (~> 2.1.0)
|
||||
- SNESDeltaCore (from `Cores/SNESDeltaCore`)
|
||||
- SQLite.swift (~> 0.12.0)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/cocoapods/specs.git:
|
||||
trunk:
|
||||
- Alamofire
|
||||
- Crashlytics
|
||||
- Fabric
|
||||
- GoogleAPIClientForREST
|
||||
- GoogleSignIn
|
||||
- GoogleToolboxForMac
|
||||
- GTMSessionFetcher
|
||||
- SDWebImage
|
||||
- SMCalloutView
|
||||
- SQLite.swift
|
||||
- SwiftyDropbox
|
||||
- ZIPFoundation
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
DeltaCore:
|
||||
:path: Cores/DeltaCore
|
||||
DSDeltaCore:
|
||||
:path: Cores/DSDeltaCore
|
||||
GBADeltaCore:
|
||||
:path: Cores/GBADeltaCore
|
||||
GBCDeltaCore:
|
||||
:path: Cores/GBCDeltaCore
|
||||
Harmony:
|
||||
:path: External/Harmony
|
||||
MelonDSDeltaCore:
|
||||
:path: Cores/MelonDSDeltaCore
|
||||
N64DeltaCore:
|
||||
:path: Cores/N64DeltaCore
|
||||
NESDeltaCore:
|
||||
:path: Cores/NESDeltaCore
|
||||
Roxas:
|
||||
:path: External/Roxas
|
||||
SNESDeltaCore:
|
||||
:path: Cores/SNESDeltaCore
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Crashlytics: 95d05f4e4c19a771250c4bd9ce344d996de32bbf
|
||||
Fabric: 2fb5676bc811af011a04513451f463dac6803206
|
||||
Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568
|
||||
Crashlytics: e156f27e43abaa331f9b7afed091bda37e1052cc
|
||||
DeltaCore: 86a78ac8fb0d40190231b5fb64d109f7516de187
|
||||
DSDeltaCore: c113b64288d253fafe350ee23df143c76a11e173
|
||||
Fabric: be0459577cee96fe21f68de24588be2dd71482b8
|
||||
GBADeltaCore: 55e32d21508ce7383a1b1587e425ccc60620ad66
|
||||
GBCDeltaCore: 7468dff742927139a14f6fc909e810f253b6293c
|
||||
GoogleAPIClientForREST: 0f19a8280dfe6471f76016645d26eb5dae305101
|
||||
GoogleSignIn: 7ff245e1a7b26d379099d3243a562f5747e23d39
|
||||
GoogleToolboxForMac: 1350d40e86a76f7863928d63bcb0b89c84c521c5
|
||||
GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52
|
||||
Harmony: cea514db17c41c22f78f54b17d2135935b5e9b96
|
||||
MelonDSDeltaCore: 3de2a2e2ebcd630a6dd756818b5a26dde7afa726
|
||||
N64DeltaCore: 6b0f07f2078193a15d736f153ce701661853fa36
|
||||
NESDeltaCore: 41ab438dd78d51d4636aacb7d9a7336ea3d4728c
|
||||
Roxas: 1990039f843f5dc284918dc82375feb80020ef62
|
||||
SDWebImage: a72e880a8fe0f7fc31efe15aaed443c074d2a80c
|
||||
SMCalloutView: 5c0ee363dc8e7204b2fda17dfad38c93e9e23481
|
||||
SQLite.swift: 46d890be8601964454bd3392527f863d1b802d45
|
||||
SNESDeltaCore: 1e9cc751501082480f8181d4449322ee4ec26dac
|
||||
SQLite.swift: d2b4642190917051ce6bd1d49aab565fe794eea3
|
||||
SwiftyDropbox: 378b4425a2e8d0cb24c7b0f2e3af72bfbaaf1e73
|
||||
ZIPFoundation: b1f0de4eed33e74a676f76e12559ab6b75990197
|
||||
|
||||
PODFILE CHECKSUM: 1d7f9ff69da571c7991621312e07aa4b16a0a074
|
||||
PODFILE CHECKSUM: 2f9219b1db18479e650fc2159f58160b4cc13aad
|
||||
|
||||
COCOAPODS: 1.6.1
|
||||
COCOAPODS: 1.10.1
|
||||
|
||||
19
Pods/Alamofire/LICENSE
generated
Normal file
19
Pods/Alamofire/LICENSE
generated
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
242
Pods/Alamofire/README.md
generated
Normal file
242
Pods/Alamofire/README.md
generated
Normal file
@ -0,0 +1,242 @@
|
||||

|
||||
|
||||
[](https://travis-ci.org/Alamofire/Alamofire)
|
||||
[](https://img.shields.io/cocoapods/v/Alamofire.svg)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://alamofire.github.io/Alamofire)
|
||||
[](https://twitter.com/AlamofireSF)
|
||||
[](https://gitter.im/Alamofire/Alamofire?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
Alamofire is an HTTP networking library written in Swift.
|
||||
|
||||
- [Features](#features)
|
||||
- [Component Libraries](#component-libraries)
|
||||
- [Requirements](#requirements)
|
||||
- [Migration Guides](#migration-guides)
|
||||
- [Communication](#communication)
|
||||
- [Installation](#installation)
|
||||
- [Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md)
|
||||
- **Intro -** [Making a Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#making-a-request), [Response Handling](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-handling), [Response Validation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-validation), [Response Caching](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-caching)
|
||||
- **HTTP -** [HTTP Methods](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-methods), [Parameter Encoding](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#parameter-encoding), [HTTP Headers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-headers), [Authentication](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#authentication)
|
||||
- **Large Data -** [Downloading Data to a File](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#downloading-data-to-a-file), [Uploading Data to a Server](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server)
|
||||
- **Tools -** [Statistical Metrics](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#statistical-metrics), [cURL Command Output](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#curl-command-output)
|
||||
- [Advanced Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md)
|
||||
- **URL Session -** [Session Manager](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-manager), [Session Delegate](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-delegate), [Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#request)
|
||||
- **Routing -** [Routing Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests), [Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests)
|
||||
- **Model Objects -** [Custom Response Serialization](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#custom-response-serialization)
|
||||
- **Connection -** [Security](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security), [Network Reachability](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#network-reachability)
|
||||
- [Open Radars](#open-radars)
|
||||
- [FAQ](#faq)
|
||||
- [Credits](#credits)
|
||||
- [Donations](#donations)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Chainable Request / Response Methods
|
||||
- [x] URL / JSON / plist Parameter Encoding
|
||||
- [x] Upload File / Data / Stream / MultipartFormData
|
||||
- [x] Download File using Request or Resume Data
|
||||
- [x] Authentication with URLCredential
|
||||
- [x] HTTP Response Validation
|
||||
- [x] Upload and Download Progress Closures with Progress
|
||||
- [x] cURL Command Output
|
||||
- [x] Dynamically Adapt and Retry Requests
|
||||
- [x] TLS Certificate and Public Key Pinning
|
||||
- [x] Network Reachability
|
||||
- [x] Comprehensive Unit and Integration Test Coverage
|
||||
- [x] [Complete Documentation](https://alamofire.github.io/Alamofire)
|
||||
|
||||
## Component Libraries
|
||||
|
||||
In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the [Alamofire Software Foundation](https://github.com/Alamofire/Foundation) to bring additional functionality to the Alamofire ecosystem.
|
||||
|
||||
- [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - An image library including image response serializers, `UIImage` and `UIImageView` extensions, custom image filters, an auto-purging in-memory cache and a priority-based image downloading system.
|
||||
- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support `URLSession` instances not managed by Alamofire.
|
||||
|
||||
## Requirements
|
||||
|
||||
- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
|
||||
- Xcode 8.3+
|
||||
- Swift 3.1+
|
||||
|
||||
## Migration Guides
|
||||
|
||||
- [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md)
|
||||
- [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md)
|
||||
- [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md)
|
||||
|
||||
## Communication
|
||||
|
||||
- If you **need help**, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire). (Tag 'alamofire')
|
||||
- If you'd like to **ask a general question**, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire).
|
||||
- If you **found a bug**, open an issue.
|
||||
- If you **have a feature request**, open an issue.
|
||||
- If you **want to contribute**, submit a pull request.
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods
|
||||
|
||||
[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command:
|
||||
|
||||
```bash
|
||||
$ gem install cocoapods
|
||||
```
|
||||
|
||||
> CocoaPods 1.1+ is required to build Alamofire 4.0+.
|
||||
|
||||
To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`:
|
||||
|
||||
```ruby
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
platform :ios, '10.0'
|
||||
use_frameworks!
|
||||
|
||||
target '<Your Target Name>' do
|
||||
pod 'Alamofire', '~> 4.7'
|
||||
end
|
||||
```
|
||||
|
||||
Then, run the following command:
|
||||
|
||||
```bash
|
||||
$ pod install
|
||||
```
|
||||
|
||||
### Carthage
|
||||
|
||||
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
|
||||
|
||||
You can install Carthage with [Homebrew](https://brew.sh/) using the following command:
|
||||
|
||||
```bash
|
||||
$ brew update
|
||||
$ brew install carthage
|
||||
```
|
||||
|
||||
To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`:
|
||||
|
||||
```ogdl
|
||||
github "Alamofire/Alamofire" ~> 4.7
|
||||
```
|
||||
|
||||
Run `carthage update` to build the framework and drag the built `Alamofire.framework` into your Xcode project.
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Alamofire does support its use on supported platforms.
|
||||
|
||||
Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
|
||||
|
||||
#### Swift 3
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4)
|
||||
]
|
||||
```
|
||||
|
||||
#### Swift 4
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.0.0")
|
||||
]
|
||||
```
|
||||
|
||||
### Manually
|
||||
|
||||
If you prefer not to use any of the aforementioned dependency managers, you can integrate Alamofire into your project manually.
|
||||
|
||||
#### Embedded Framework
|
||||
|
||||
- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
|
||||
|
||||
```bash
|
||||
$ git init
|
||||
```
|
||||
|
||||
- Add Alamofire as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command:
|
||||
|
||||
```bash
|
||||
$ git submodule add https://github.com/Alamofire/Alamofire.git
|
||||
```
|
||||
|
||||
- Open the new `Alamofire` folder, and drag the `Alamofire.xcodeproj` into the Project Navigator of your application's Xcode project.
|
||||
|
||||
> It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
|
||||
|
||||
- Select the `Alamofire.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
|
||||
- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
|
||||
- In the tab bar at the top of that window, open the "General" panel.
|
||||
- Click on the `+` button under the "Embedded Binaries" section.
|
||||
- You will see two different `Alamofire.xcodeproj` folders each with two different versions of the `Alamofire.framework` nested inside a `Products` folder.
|
||||
|
||||
> It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `Alamofire.framework`.
|
||||
|
||||
- Select the top `Alamofire.framework` for iOS and the bottom one for OS X.
|
||||
|
||||
> You can verify which one you selected by inspecting the build log for your project. The build target for `Alamofire` will be listed as either `Alamofire iOS`, `Alamofire macOS`, `Alamofire tvOS` or `Alamofire watchOS`.
|
||||
|
||||
- And that's it!
|
||||
|
||||
> The `Alamofire.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
|
||||
|
||||
## Open Radars
|
||||
|
||||
The following radars have some effect on the current implementation of Alamofire.
|
||||
|
||||
- [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in test case
|
||||
- `rdar://26870455` - Background URL Session Configurations do not work in the simulator
|
||||
- `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest`
|
||||
- [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+
|
||||
|
||||
## Resolved Radars
|
||||
|
||||
The following radars have been resolved over time after being filed against the Alamofire project.
|
||||
|
||||
- [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage (Resolved on 9/1/17 in Xcode 9 beta 6).
|
||||
|
||||
## FAQ
|
||||
|
||||
### What's the origin of the name Alamofire?
|
||||
|
||||
Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu.edu/wildseed/alamofire.html), a hybrid variant of the Bluebonnet, the official state flower of Texas.
|
||||
|
||||
### What logic belongs in a Router vs. a Request Adapter?
|
||||
|
||||
Simple, static data such as paths, parameters and common headers belong in the `Router`. Dynamic data such as an `Authorization` header whose value can changed based on an authentication system belongs in a `RequestAdapter`.
|
||||
|
||||
The reason the dynamic data MUST be placed into the `RequestAdapter` is to support retry operations. When a `Request` is retried, the original request is not rebuilt meaning the `Router` will not be called again. The `RequestAdapter` is called again allowing the dynamic data to be updated on the original request before retrying the `Request`.
|
||||
|
||||
## Credits
|
||||
|
||||
Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases.
|
||||
|
||||
### Security Disclosure
|
||||
|
||||
If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker.
|
||||
|
||||
## Donations
|
||||
|
||||
The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization.
|
||||
Registering will allow us members to gain some legal protections and also allow us to put donations to use, tax free.
|
||||
Donating to the ASF will enable us to:
|
||||
|
||||
- Pay our yearly legal fees to keep the non-profit in good status
|
||||
- Pay for our mail servers to help us stay on top of all questions and security issues
|
||||
- Potentially fund test servers to make it easier for us to test the edge cases
|
||||
- Potentially fund developers to work on one of our projects full-time
|
||||
|
||||
The community adoption of the ASF libraries has been amazing.
|
||||
We are greatly humbled by your enthusiasm around the projects, and want to continue to do everything we can to move the needle forward.
|
||||
With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members.
|
||||
If you use any of our libraries for work, see if your employers would be interested in donating.
|
||||
Any amount you can donate today to help us reach our goal would be greatly appreciated.
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W34WPEE74APJQ)
|
||||
|
||||
## License
|
||||
|
||||
Alamofire is released under the MIT license. [See LICENSE](https://github.com/Alamofire/Alamofire/blob/master/LICENSE) for details.
|
||||
460
Pods/Alamofire/Source/AFError.swift
generated
Normal file
460
Pods/Alamofire/Source/AFError.swift
generated
Normal file
@ -0,0 +1,460 @@
|
||||
//
|
||||
// AFError.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with
|
||||
/// their own associated reasons.
|
||||
///
|
||||
/// - invalidURL: Returned when a `URLConvertible` type fails to create a valid `URL`.
|
||||
/// - parameterEncodingFailed: Returned when a parameter encoding object throws an error during the encoding process.
|
||||
/// - multipartEncodingFailed: Returned when some step in the multipart encoding process fails.
|
||||
/// - responseValidationFailed: Returned when a `validate()` call fails.
|
||||
/// - responseSerializationFailed: Returned when a response serializer encounters an error in the serialization process.
|
||||
public enum AFError: Error {
|
||||
/// The underlying reason the parameter encoding error occurred.
|
||||
///
|
||||
/// - missingURL: The URL request did not have a URL to encode.
|
||||
/// - jsonEncodingFailed: JSON serialization failed with an underlying system error during the
|
||||
/// encoding process.
|
||||
/// - propertyListEncodingFailed: Property list serialization failed with an underlying system error during
|
||||
/// encoding process.
|
||||
public enum ParameterEncodingFailureReason {
|
||||
case missingURL
|
||||
case jsonEncodingFailed(error: Error)
|
||||
case propertyListEncodingFailed(error: Error)
|
||||
}
|
||||
|
||||
/// The underlying reason the multipart encoding error occurred.
|
||||
///
|
||||
/// - bodyPartURLInvalid: The `fileURL` provided for reading an encodable body part isn't a
|
||||
/// file URL.
|
||||
/// - bodyPartFilenameInvalid: The filename of the `fileURL` provided has either an empty
|
||||
/// `lastPathComponent` or `pathExtension.
|
||||
/// - bodyPartFileNotReachable: The file at the `fileURL` provided was not reachable.
|
||||
/// - bodyPartFileNotReachableWithError: Attempting to check the reachability of the `fileURL` provided threw
|
||||
/// an error.
|
||||
/// - bodyPartFileIsDirectory: The file at the `fileURL` provided is actually a directory.
|
||||
/// - bodyPartFileSizeNotAvailable: The size of the file at the `fileURL` provided was not returned by
|
||||
/// the system.
|
||||
/// - bodyPartFileSizeQueryFailedWithError: The attempt to find the size of the file at the `fileURL` provided
|
||||
/// threw an error.
|
||||
/// - bodyPartInputStreamCreationFailed: An `InputStream` could not be created for the provided `fileURL`.
|
||||
/// - outputStreamCreationFailed: An `OutputStream` could not be created when attempting to write the
|
||||
/// encoded data to disk.
|
||||
/// - outputStreamFileAlreadyExists: The encoded body data could not be writtent disk because a file
|
||||
/// already exists at the provided `fileURL`.
|
||||
/// - outputStreamURLInvalid: The `fileURL` provided for writing the encoded body data to disk is
|
||||
/// not a file URL.
|
||||
/// - outputStreamWriteFailed: The attempt to write the encoded body data to disk failed with an
|
||||
/// underlying error.
|
||||
/// - inputStreamReadFailed: The attempt to read an encoded body part `InputStream` failed with
|
||||
/// underlying system error.
|
||||
public enum MultipartEncodingFailureReason {
|
||||
case bodyPartURLInvalid(url: URL)
|
||||
case bodyPartFilenameInvalid(in: URL)
|
||||
case bodyPartFileNotReachable(at: URL)
|
||||
case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
|
||||
case bodyPartFileIsDirectory(at: URL)
|
||||
case bodyPartFileSizeNotAvailable(at: URL)
|
||||
case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
|
||||
case bodyPartInputStreamCreationFailed(for: URL)
|
||||
|
||||
case outputStreamCreationFailed(for: URL)
|
||||
case outputStreamFileAlreadyExists(at: URL)
|
||||
case outputStreamURLInvalid(url: URL)
|
||||
case outputStreamWriteFailed(error: Error)
|
||||
|
||||
case inputStreamReadFailed(error: Error)
|
||||
}
|
||||
|
||||
/// The underlying reason the response validation error occurred.
|
||||
///
|
||||
/// - dataFileNil: The data file containing the server response did not exist.
|
||||
/// - dataFileReadFailed: The data file containing the server response could not be read.
|
||||
/// - missingContentType: The response did not contain a `Content-Type` and the `acceptableContentTypes`
|
||||
/// provided did not contain wildcard type.
|
||||
/// - unacceptableContentType: The response `Content-Type` did not match any type in the provided
|
||||
/// `acceptableContentTypes`.
|
||||
/// - unacceptableStatusCode: The response status code was not acceptable.
|
||||
public enum ResponseValidationFailureReason {
|
||||
case dataFileNil
|
||||
case dataFileReadFailed(at: URL)
|
||||
case missingContentType(acceptableContentTypes: [String])
|
||||
case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
|
||||
case unacceptableStatusCode(code: Int)
|
||||
}
|
||||
|
||||
/// The underlying reason the response serialization error occurred.
|
||||
///
|
||||
/// - inputDataNil: The server response contained no data.
|
||||
/// - inputDataNilOrZeroLength: The server response contained no data or the data was zero length.
|
||||
/// - inputFileNil: The file containing the server response did not exist.
|
||||
/// - inputFileReadFailed: The file containing the server response could not be read.
|
||||
/// - stringSerializationFailed: String serialization failed using the provided `String.Encoding`.
|
||||
/// - jsonSerializationFailed: JSON serialization failed with an underlying system error.
|
||||
/// - propertyListSerializationFailed: Property list serialization failed with an underlying system error.
|
||||
public enum ResponseSerializationFailureReason {
|
||||
case inputDataNil
|
||||
case inputDataNilOrZeroLength
|
||||
case inputFileNil
|
||||
case inputFileReadFailed(at: URL)
|
||||
case stringSerializationFailed(encoding: String.Encoding)
|
||||
case jsonSerializationFailed(error: Error)
|
||||
case propertyListSerializationFailed(error: Error)
|
||||
}
|
||||
|
||||
case invalidURL(url: URLConvertible)
|
||||
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
|
||||
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
|
||||
case responseValidationFailed(reason: ResponseValidationFailureReason)
|
||||
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
|
||||
}
|
||||
|
||||
// MARK: - Adapt Error
|
||||
|
||||
struct AdaptError: Error {
|
||||
let error: Error
|
||||
}
|
||||
|
||||
extension Error {
|
||||
var underlyingAdaptError: Error? { return (self as? AdaptError)?.error }
|
||||
}
|
||||
|
||||
// MARK: - Error Booleans
|
||||
|
||||
extension AFError {
|
||||
/// Returns whether the AFError is an invalid URL error.
|
||||
public var isInvalidURLError: Bool {
|
||||
if case .invalidURL = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns whether the AFError is a parameter encoding error. When `true`, the `underlyingError` property will
|
||||
/// contain the associated value.
|
||||
public var isParameterEncodingError: Bool {
|
||||
if case .parameterEncodingFailed = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns whether the AFError is a multipart encoding error. When `true`, the `url` and `underlyingError` properties
|
||||
/// will contain the associated values.
|
||||
public var isMultipartEncodingError: Bool {
|
||||
if case .multipartEncodingFailed = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns whether the `AFError` is a response validation error. When `true`, the `acceptableContentTypes`,
|
||||
/// `responseContentType`, and `responseCode` properties will contain the associated values.
|
||||
public var isResponseValidationError: Bool {
|
||||
if case .responseValidationFailed = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns whether the `AFError` is a response serialization error. When `true`, the `failedStringEncoding` and
|
||||
/// `underlyingError` properties will contain the associated values.
|
||||
public var isResponseSerializationError: Bool {
|
||||
if case .responseSerializationFailed = self { return true }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Properties
|
||||
|
||||
extension AFError {
|
||||
/// The `URLConvertible` associated with the error.
|
||||
public var urlConvertible: URLConvertible? {
|
||||
switch self {
|
||||
case .invalidURL(let url):
|
||||
return url
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The `URL` associated with the error.
|
||||
public var url: URL? {
|
||||
switch self {
|
||||
case .multipartEncodingFailed(let reason):
|
||||
return reason.url
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Error` returned by a system framework associated with a `.parameterEncodingFailed`,
|
||||
/// `.multipartEncodingFailed` or `.responseSerializationFailed` error.
|
||||
public var underlyingError: Error? {
|
||||
switch self {
|
||||
case .parameterEncodingFailed(let reason):
|
||||
return reason.underlyingError
|
||||
case .multipartEncodingFailed(let reason):
|
||||
return reason.underlyingError
|
||||
case .responseSerializationFailed(let reason):
|
||||
return reason.underlyingError
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The acceptable `Content-Type`s of a `.responseValidationFailed` error.
|
||||
public var acceptableContentTypes: [String]? {
|
||||
switch self {
|
||||
case .responseValidationFailed(let reason):
|
||||
return reason.acceptableContentTypes
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The response `Content-Type` of a `.responseValidationFailed` error.
|
||||
public var responseContentType: String? {
|
||||
switch self {
|
||||
case .responseValidationFailed(let reason):
|
||||
return reason.responseContentType
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The response code of a `.responseValidationFailed` error.
|
||||
public var responseCode: Int? {
|
||||
switch self {
|
||||
case .responseValidationFailed(let reason):
|
||||
return reason.responseCode
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The `String.Encoding` associated with a failed `.stringResponse()` call.
|
||||
public var failedStringEncoding: String.Encoding? {
|
||||
switch self {
|
||||
case .responseSerializationFailed(let reason):
|
||||
return reason.failedStringEncoding
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.ParameterEncodingFailureReason {
|
||||
var underlyingError: Error? {
|
||||
switch self {
|
||||
case .jsonEncodingFailed(let error), .propertyListEncodingFailed(let error):
|
||||
return error
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.MultipartEncodingFailureReason {
|
||||
var url: URL? {
|
||||
switch self {
|
||||
case .bodyPartURLInvalid(let url), .bodyPartFilenameInvalid(let url), .bodyPartFileNotReachable(let url),
|
||||
.bodyPartFileIsDirectory(let url), .bodyPartFileSizeNotAvailable(let url),
|
||||
.bodyPartInputStreamCreationFailed(let url), .outputStreamCreationFailed(let url),
|
||||
.outputStreamFileAlreadyExists(let url), .outputStreamURLInvalid(let url),
|
||||
.bodyPartFileNotReachableWithError(let url, _), .bodyPartFileSizeQueryFailedWithError(let url, _):
|
||||
return url
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var underlyingError: Error? {
|
||||
switch self {
|
||||
case .bodyPartFileNotReachableWithError(_, let error), .bodyPartFileSizeQueryFailedWithError(_, let error),
|
||||
.outputStreamWriteFailed(let error), .inputStreamReadFailed(let error):
|
||||
return error
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.ResponseValidationFailureReason {
|
||||
var acceptableContentTypes: [String]? {
|
||||
switch self {
|
||||
case .missingContentType(let types), .unacceptableContentType(let types, _):
|
||||
return types
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var responseContentType: String? {
|
||||
switch self {
|
||||
case .unacceptableContentType(_, let responseType):
|
||||
return responseType
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var responseCode: Int? {
|
||||
switch self {
|
||||
case .unacceptableStatusCode(let code):
|
||||
return code
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.ResponseSerializationFailureReason {
|
||||
var failedStringEncoding: String.Encoding? {
|
||||
switch self {
|
||||
case .stringSerializationFailed(let encoding):
|
||||
return encoding
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var underlyingError: Error? {
|
||||
switch self {
|
||||
case .jsonSerializationFailed(let error), .propertyListSerializationFailed(let error):
|
||||
return error
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Descriptions
|
||||
|
||||
extension AFError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL(let url):
|
||||
return "URL is not valid: \(url)"
|
||||
case .parameterEncodingFailed(let reason):
|
||||
return reason.localizedDescription
|
||||
case .multipartEncodingFailed(let reason):
|
||||
return reason.localizedDescription
|
||||
case .responseValidationFailed(let reason):
|
||||
return reason.localizedDescription
|
||||
case .responseSerializationFailed(let reason):
|
||||
return reason.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.ParameterEncodingFailureReason {
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .missingURL:
|
||||
return "URL request to encode was missing a URL"
|
||||
case .jsonEncodingFailed(let error):
|
||||
return "JSON could not be encoded because of error:\n\(error.localizedDescription)"
|
||||
case .propertyListEncodingFailed(let error):
|
||||
return "PropertyList could not be encoded because of error:\n\(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.MultipartEncodingFailureReason {
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .bodyPartURLInvalid(let url):
|
||||
return "The URL provided is not a file URL: \(url)"
|
||||
case .bodyPartFilenameInvalid(let url):
|
||||
return "The URL provided does not have a valid filename: \(url)"
|
||||
case .bodyPartFileNotReachable(let url):
|
||||
return "The URL provided is not reachable: \(url)"
|
||||
case .bodyPartFileNotReachableWithError(let url, let error):
|
||||
return (
|
||||
"The system returned an error while checking the provided URL for " +
|
||||
"reachability.\nURL: \(url)\nError: \(error)"
|
||||
)
|
||||
case .bodyPartFileIsDirectory(let url):
|
||||
return "The URL provided is a directory: \(url)"
|
||||
case .bodyPartFileSizeNotAvailable(let url):
|
||||
return "Could not fetch the file size from the provided URL: \(url)"
|
||||
case .bodyPartFileSizeQueryFailedWithError(let url, let error):
|
||||
return (
|
||||
"The system returned an error while attempting to fetch the file size from the " +
|
||||
"provided URL.\nURL: \(url)\nError: \(error)"
|
||||
)
|
||||
case .bodyPartInputStreamCreationFailed(let url):
|
||||
return "Failed to create an InputStream for the provided URL: \(url)"
|
||||
case .outputStreamCreationFailed(let url):
|
||||
return "Failed to create an OutputStream for URL: \(url)"
|
||||
case .outputStreamFileAlreadyExists(let url):
|
||||
return "A file already exists at the provided URL: \(url)"
|
||||
case .outputStreamURLInvalid(let url):
|
||||
return "The provided OutputStream URL is invalid: \(url)"
|
||||
case .outputStreamWriteFailed(let error):
|
||||
return "OutputStream write failed with error: \(error)"
|
||||
case .inputStreamReadFailed(let error):
|
||||
return "InputStream read failed with error: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.ResponseSerializationFailureReason {
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .inputDataNil:
|
||||
return "Response could not be serialized, input data was nil."
|
||||
case .inputDataNilOrZeroLength:
|
||||
return "Response could not be serialized, input data was nil or zero length."
|
||||
case .inputFileNil:
|
||||
return "Response could not be serialized, input file was nil."
|
||||
case .inputFileReadFailed(let url):
|
||||
return "Response could not be serialized, input file could not be read: \(url)."
|
||||
case .stringSerializationFailed(let encoding):
|
||||
return "String could not be serialized with encoding: \(encoding)."
|
||||
case .jsonSerializationFailed(let error):
|
||||
return "JSON could not be serialized because of error:\n\(error.localizedDescription)"
|
||||
case .propertyListSerializationFailed(let error):
|
||||
return "PropertyList could not be serialized because of error:\n\(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AFError.ResponseValidationFailureReason {
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .dataFileNil:
|
||||
return "Response could not be validated, data file was nil."
|
||||
case .dataFileReadFailed(let url):
|
||||
return "Response could not be validated, data file could not be read: \(url)."
|
||||
case .missingContentType(let types):
|
||||
return (
|
||||
"Response Content-Type was missing and acceptable content types " +
|
||||
"(\(types.joined(separator: ","))) do not match \"*/*\"."
|
||||
)
|
||||
case .unacceptableContentType(let acceptableTypes, let responseType):
|
||||
return (
|
||||
"Response Content-Type \"\(responseType)\" does not match any acceptable types: " +
|
||||
"\(acceptableTypes.joined(separator: ","))."
|
||||
)
|
||||
case .unacceptableStatusCode(let code):
|
||||
return "Response status code was unacceptable: \(code)."
|
||||
}
|
||||
}
|
||||
}
|
||||
465
Pods/Alamofire/Source/Alamofire.swift
generated
Normal file
465
Pods/Alamofire/Source/Alamofire.swift
generated
Normal file
@ -0,0 +1,465 @@
|
||||
//
|
||||
// Alamofire.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Types adopting the `URLConvertible` protocol can be used to construct URLs, which are then used to construct
|
||||
/// URL requests.
|
||||
public protocol URLConvertible {
|
||||
/// Returns a URL that conforms to RFC 2396 or throws an `Error`.
|
||||
///
|
||||
/// - throws: An `Error` if the type cannot be converted to a `URL`.
|
||||
///
|
||||
/// - returns: A URL or throws an `Error`.
|
||||
func asURL() throws -> URL
|
||||
}
|
||||
|
||||
extension String: URLConvertible {
|
||||
/// Returns a URL if `self` represents a valid URL string that conforms to RFC 2396 or throws an `AFError`.
|
||||
///
|
||||
/// - throws: An `AFError.invalidURL` if `self` is not a valid URL string.
|
||||
///
|
||||
/// - returns: A URL or throws an `AFError`.
|
||||
public func asURL() throws -> URL {
|
||||
guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
extension URL: URLConvertible {
|
||||
/// Returns self.
|
||||
public func asURL() throws -> URL { return self }
|
||||
}
|
||||
|
||||
extension URLComponents: URLConvertible {
|
||||
/// Returns a URL if `url` is not nil, otherwise throws an `Error`.
|
||||
///
|
||||
/// - throws: An `AFError.invalidURL` if `url` is `nil`.
|
||||
///
|
||||
/// - returns: A URL or throws an `AFError`.
|
||||
public func asURL() throws -> URL {
|
||||
guard let url = url else { throw AFError.invalidURL(url: self) }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Types adopting the `URLRequestConvertible` protocol can be used to construct URL requests.
|
||||
public protocol URLRequestConvertible {
|
||||
/// Returns a URL request or throws if an `Error` was encountered.
|
||||
///
|
||||
/// - throws: An `Error` if the underlying `URLRequest` is `nil`.
|
||||
///
|
||||
/// - returns: A URL request.
|
||||
func asURLRequest() throws -> URLRequest
|
||||
}
|
||||
|
||||
extension URLRequestConvertible {
|
||||
/// The URL request.
|
||||
public var urlRequest: URLRequest? { return try? asURLRequest() }
|
||||
}
|
||||
|
||||
extension URLRequest: URLRequestConvertible {
|
||||
/// Returns a URL request or throws if an `Error` was encountered.
|
||||
public func asURLRequest() throws -> URLRequest { return self }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension URLRequest {
|
||||
/// Creates an instance with the specified `method`, `urlString` and `headers`.
|
||||
///
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The new `URLRequest` instance.
|
||||
public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws {
|
||||
let url = try url.asURL()
|
||||
|
||||
self.init(url: url)
|
||||
|
||||
httpMethod = method.rawValue
|
||||
|
||||
if let headers = headers {
|
||||
for (headerField, headerValue) in headers {
|
||||
setValue(headerValue, forHTTPHeaderField: headerField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adapt(using adapter: RequestAdapter?) throws -> URLRequest {
|
||||
guard let adapter = adapter else { return self }
|
||||
return try adapter.adapt(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data Request
|
||||
|
||||
/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
|
||||
/// `method`, `parameters`, `encoding` and `headers`.
|
||||
///
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.get` by default.
|
||||
/// - parameter parameters: The parameters. `nil` by default.
|
||||
/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DataRequest`.
|
||||
@discardableResult
|
||||
public func request(
|
||||
_ url: URLConvertible,
|
||||
method: HTTPMethod = .get,
|
||||
parameters: Parameters? = nil,
|
||||
encoding: ParameterEncoding = URLEncoding.default,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> DataRequest
|
||||
{
|
||||
return SessionManager.default.request(
|
||||
url,
|
||||
method: method,
|
||||
parameters: parameters,
|
||||
encoding: encoding,
|
||||
headers: headers
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
|
||||
/// specified `urlRequest`.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request
|
||||
///
|
||||
/// - returns: The created `DataRequest`.
|
||||
@discardableResult
|
||||
public func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
|
||||
return SessionManager.default.request(urlRequest)
|
||||
}
|
||||
|
||||
// MARK: - Download Request
|
||||
|
||||
// MARK: URL Request
|
||||
|
||||
/// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
|
||||
/// `method`, `parameters`, `encoding`, `headers` and save them to the `destination`.
|
||||
///
|
||||
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
|
||||
/// underlying URL session.
|
||||
///
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.get` by default.
|
||||
/// - parameter parameters: The parameters. `nil` by default.
|
||||
/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DownloadRequest`.
|
||||
@discardableResult
|
||||
public func download(
|
||||
_ url: URLConvertible,
|
||||
method: HTTPMethod = .get,
|
||||
parameters: Parameters? = nil,
|
||||
encoding: ParameterEncoding = URLEncoding.default,
|
||||
headers: HTTPHeaders? = nil,
|
||||
to destination: DownloadRequest.DownloadFileDestination? = nil)
|
||||
-> DownloadRequest
|
||||
{
|
||||
return SessionManager.default.download(
|
||||
url,
|
||||
method: method,
|
||||
parameters: parameters,
|
||||
encoding: encoding,
|
||||
headers: headers,
|
||||
to: destination
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `DownloadRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
|
||||
/// specified `urlRequest` and save them to the `destination`.
|
||||
///
|
||||
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
|
||||
/// underlying URL session.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DownloadRequest`.
|
||||
@discardableResult
|
||||
public func download(
|
||||
_ urlRequest: URLRequestConvertible,
|
||||
to destination: DownloadRequest.DownloadFileDestination? = nil)
|
||||
-> DownloadRequest
|
||||
{
|
||||
return SessionManager.default.download(urlRequest, to: destination)
|
||||
}
|
||||
|
||||
// MARK: Resume Data
|
||||
|
||||
/// Creates a `DownloadRequest` using the default `SessionManager` from the `resumeData` produced from a
|
||||
/// previous request cancellation to retrieve the contents of the original request and save them to the `destination`.
|
||||
///
|
||||
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
|
||||
/// underlying URL session.
|
||||
///
|
||||
/// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken
|
||||
/// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the
|
||||
/// data is written incorrectly and will always fail to resume the download. For more information about the bug and
|
||||
/// possible workarounds, please refer to the following Stack Overflow post:
|
||||
///
|
||||
/// - http://stackoverflow.com/a/39347461/1342462
|
||||
///
|
||||
/// - parameter resumeData: The resume data. This is an opaque data blob produced by `URLSessionDownloadTask`
|
||||
/// when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for additional
|
||||
/// information.
|
||||
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DownloadRequest`.
|
||||
@discardableResult
|
||||
public func download(
|
||||
resumingWith resumeData: Data,
|
||||
to destination: DownloadRequest.DownloadFileDestination? = nil)
|
||||
-> DownloadRequest
|
||||
{
|
||||
return SessionManager.default.download(resumingWith: resumeData, to: destination)
|
||||
}
|
||||
|
||||
// MARK: - Upload Request
|
||||
|
||||
// MARK: File
|
||||
|
||||
/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
|
||||
/// for uploading the `file`.
|
||||
///
|
||||
/// - parameter file: The file to upload.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
public func upload(
|
||||
_ fileURL: URL,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> UploadRequest
|
||||
{
|
||||
return SessionManager.default.upload(fileURL, to: url, method: method, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates a `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
|
||||
/// uploading the `file`.
|
||||
///
|
||||
/// - parameter file: The file to upload.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
public func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest {
|
||||
return SessionManager.default.upload(fileURL, with: urlRequest)
|
||||
}
|
||||
|
||||
// MARK: Data
|
||||
|
||||
/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
|
||||
/// for uploading the `data`.
|
||||
///
|
||||
/// - parameter data: The data to upload.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
public func upload(
|
||||
_ data: Data,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> UploadRequest
|
||||
{
|
||||
return SessionManager.default.upload(data, to: url, method: method, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
|
||||
/// uploading the `data`.
|
||||
///
|
||||
/// - parameter data: The data to upload.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
public func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
|
||||
return SessionManager.default.upload(data, with: urlRequest)
|
||||
}
|
||||
|
||||
// MARK: InputStream
|
||||
|
||||
/// Creates an `UploadRequest` using the default `SessionManager` from the specified `url`, `method` and `headers`
|
||||
/// for uploading the `stream`.
|
||||
///
|
||||
/// - parameter stream: The stream to upload.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
public func upload(
|
||||
_ stream: InputStream,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> UploadRequest
|
||||
{
|
||||
return SessionManager.default.upload(stream, to: url, method: method, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates an `UploadRequest` using the default `SessionManager` from the specified `urlRequest` for
|
||||
/// uploading the `stream`.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter stream: The stream to upload.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
public func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest {
|
||||
return SessionManager.default.upload(stream, with: urlRequest)
|
||||
}
|
||||
|
||||
// MARK: MultipartFormData
|
||||
|
||||
/// Encodes `multipartFormData` using `encodingMemoryThreshold` with the default `SessionManager` and calls
|
||||
/// `encodingCompletion` with new `UploadRequest` using the `url`, `method` and `headers`.
|
||||
///
|
||||
/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
|
||||
/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
|
||||
/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
|
||||
/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
|
||||
/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
|
||||
/// used for larger payloads such as video content.
|
||||
///
|
||||
/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
|
||||
/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
|
||||
/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
|
||||
/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
|
||||
/// technique was used.
|
||||
///
|
||||
/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`.
|
||||
/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
|
||||
/// `multipartFormDataEncodingMemoryThreshold` by default.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete.
|
||||
public func upload(
|
||||
multipartFormData: @escaping (MultipartFormData) -> Void,
|
||||
usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil,
|
||||
encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
|
||||
{
|
||||
return SessionManager.default.upload(
|
||||
multipartFormData: multipartFormData,
|
||||
usingThreshold: encodingMemoryThreshold,
|
||||
to: url,
|
||||
method: method,
|
||||
headers: headers,
|
||||
encodingCompletion: encodingCompletion
|
||||
)
|
||||
}
|
||||
|
||||
/// Encodes `multipartFormData` using `encodingMemoryThreshold` and the default `SessionManager` and
|
||||
/// calls `encodingCompletion` with new `UploadRequest` using the `urlRequest`.
|
||||
///
|
||||
/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
|
||||
/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
|
||||
/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
|
||||
/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
|
||||
/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
|
||||
/// used for larger payloads such as video content.
|
||||
///
|
||||
/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
|
||||
/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
|
||||
/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
|
||||
/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
|
||||
/// technique was used.
|
||||
///
|
||||
/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`.
|
||||
/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
|
||||
/// `multipartFormDataEncodingMemoryThreshold` by default.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete.
|
||||
public func upload(
|
||||
multipartFormData: @escaping (MultipartFormData) -> Void,
|
||||
usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
|
||||
with urlRequest: URLRequestConvertible,
|
||||
encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
|
||||
{
|
||||
return SessionManager.default.upload(
|
||||
multipartFormData: multipartFormData,
|
||||
usingThreshold: encodingMemoryThreshold,
|
||||
with: urlRequest,
|
||||
encodingCompletion: encodingCompletion
|
||||
)
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
// MARK: - Stream Request
|
||||
|
||||
// MARK: Hostname and Port
|
||||
|
||||
/// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `hostname`
|
||||
/// and `port`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter hostName: The hostname of the server to connect to.
|
||||
/// - parameter port: The port of the server to connect to.
|
||||
///
|
||||
/// - returns: The created `StreamRequest`.
|
||||
@discardableResult
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
public func stream(withHostName hostName: String, port: Int) -> StreamRequest {
|
||||
return SessionManager.default.stream(withHostName: hostName, port: port)
|
||||
}
|
||||
|
||||
// MARK: NetService
|
||||
|
||||
/// Creates a `StreamRequest` using the default `SessionManager` for bidirectional streaming with the `netService`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter netService: The net service used to identify the endpoint.
|
||||
///
|
||||
/// - returns: The created `StreamRequest`.
|
||||
@discardableResult
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
public func stream(with netService: NetService) -> StreamRequest {
|
||||
return SessionManager.default.stream(with: netService)
|
||||
}
|
||||
|
||||
#endif
|
||||
37
Pods/Alamofire/Source/DispatchQueue+Alamofire.swift
generated
Normal file
37
Pods/Alamofire/Source/DispatchQueue+Alamofire.swift
generated
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// DispatchQueue+Alamofire.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
extension DispatchQueue {
|
||||
static var userInteractive: DispatchQueue { return DispatchQueue.global(qos: .userInteractive) }
|
||||
static var userInitiated: DispatchQueue { return DispatchQueue.global(qos: .userInitiated) }
|
||||
static var utility: DispatchQueue { return DispatchQueue.global(qos: .utility) }
|
||||
static var background: DispatchQueue { return DispatchQueue.global(qos: .background) }
|
||||
|
||||
func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) {
|
||||
asyncAfter(deadline: .now() + delay, execute: closure)
|
||||
}
|
||||
}
|
||||
580
Pods/Alamofire/Source/MultipartFormData.swift
generated
Normal file
580
Pods/Alamofire/Source/MultipartFormData.swift
generated
Normal file
@ -0,0 +1,580 @@
|
||||
//
|
||||
// MultipartFormData.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(watchOS) || os(tvOS)
|
||||
import MobileCoreServices
|
||||
#elseif os(macOS)
|
||||
import CoreServices
|
||||
#endif
|
||||
|
||||
/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
|
||||
/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
|
||||
/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
|
||||
/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
|
||||
/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
|
||||
///
|
||||
/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
|
||||
/// and the w3 form documentation.
|
||||
///
|
||||
/// - https://www.ietf.org/rfc/rfc2388.txt
|
||||
/// - https://www.ietf.org/rfc/rfc2045.txt
|
||||
/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
|
||||
open class MultipartFormData {
|
||||
|
||||
// MARK: - Helper Types
|
||||
|
||||
struct EncodingCharacters {
|
||||
static let crlf = "\r\n"
|
||||
}
|
||||
|
||||
struct BoundaryGenerator {
|
||||
enum BoundaryType {
|
||||
case initial, encapsulated, final
|
||||
}
|
||||
|
||||
static func randomBoundary() -> String {
|
||||
return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
|
||||
}
|
||||
|
||||
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
|
||||
let boundaryText: String
|
||||
|
||||
switch boundaryType {
|
||||
case .initial:
|
||||
boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
|
||||
case .encapsulated:
|
||||
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
|
||||
case .final:
|
||||
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
|
||||
}
|
||||
|
||||
return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
|
||||
}
|
||||
}
|
||||
|
||||
class BodyPart {
|
||||
let headers: HTTPHeaders
|
||||
let bodyStream: InputStream
|
||||
let bodyContentLength: UInt64
|
||||
var hasInitialBoundary = false
|
||||
var hasFinalBoundary = false
|
||||
|
||||
init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
|
||||
self.headers = headers
|
||||
self.bodyStream = bodyStream
|
||||
self.bodyContentLength = bodyContentLength
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
|
||||
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
|
||||
|
||||
/// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries.
|
||||
public var contentLength: UInt64 { return bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
|
||||
|
||||
/// The boundary used to separate the body parts in the encoded form data.
|
||||
public let boundary: String
|
||||
|
||||
private var bodyParts: [BodyPart]
|
||||
private var bodyPartError: AFError?
|
||||
private let streamBufferSize: Int
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Creates a multipart form data object.
|
||||
///
|
||||
/// - returns: The multipart form data object.
|
||||
public init() {
|
||||
self.boundary = BoundaryGenerator.randomBoundary()
|
||||
self.bodyParts = []
|
||||
|
||||
///
|
||||
/// The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
|
||||
/// information, please refer to the following article:
|
||||
/// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
|
||||
///
|
||||
|
||||
self.streamBufferSize = 1024
|
||||
}
|
||||
|
||||
// MARK: - Body Parts
|
||||
|
||||
/// Creates a body part from the data and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
|
||||
/// - Encoded data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// - parameter data: The data to encode into the multipart form data.
|
||||
/// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
|
||||
public func append(_ data: Data, withName name: String) {
|
||||
let headers = contentHeaders(withName: name)
|
||||
let stream = InputStream(data: data)
|
||||
let length = UInt64(data.count)
|
||||
|
||||
append(stream, withLength: length, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates a body part from the data and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
|
||||
/// - `Content-Type: #{generated mimeType}` (HTTP Header)
|
||||
/// - Encoded data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// - parameter data: The data to encode into the multipart form data.
|
||||
/// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
|
||||
/// - parameter mimeType: The MIME type to associate with the data content type in the `Content-Type` HTTP header.
|
||||
public func append(_ data: Data, withName name: String, mimeType: String) {
|
||||
let headers = contentHeaders(withName: name, mimeType: mimeType)
|
||||
let stream = InputStream(data: data)
|
||||
let length = UInt64(data.count)
|
||||
|
||||
append(stream, withLength: length, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates a body part from the data and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
|
||||
/// - `Content-Type: #{mimeType}` (HTTP Header)
|
||||
/// - Encoded file data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// - parameter data: The data to encode into the multipart form data.
|
||||
/// - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
|
||||
/// - parameter fileName: The filename to associate with the data in the `Content-Disposition` HTTP header.
|
||||
/// - parameter mimeType: The MIME type to associate with the data in the `Content-Type` HTTP header.
|
||||
public func append(_ data: Data, withName name: String, fileName: String, mimeType: String) {
|
||||
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
|
||||
let stream = InputStream(data: data)
|
||||
let length = UInt64(data.count)
|
||||
|
||||
append(stream, withLength: length, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates a body part from the file and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
|
||||
/// - `Content-Type: #{generated mimeType}` (HTTP Header)
|
||||
/// - Encoded file data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
|
||||
/// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
|
||||
/// system associated MIME type.
|
||||
///
|
||||
/// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
|
||||
/// - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header.
|
||||
public func append(_ fileURL: URL, withName name: String) {
|
||||
let fileName = fileURL.lastPathComponent
|
||||
let pathExtension = fileURL.pathExtension
|
||||
|
||||
if !fileName.isEmpty && !pathExtension.isEmpty {
|
||||
let mime = mimeType(forPathExtension: pathExtension)
|
||||
append(fileURL, withName: name, fileName: fileName, mimeType: mime)
|
||||
} else {
|
||||
setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a body part from the file and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
|
||||
/// - Content-Type: #{mimeType} (HTTP Header)
|
||||
/// - Encoded file data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
|
||||
/// - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header.
|
||||
/// - parameter fileName: The filename to associate with the file content in the `Content-Disposition` HTTP header.
|
||||
/// - parameter mimeType: The MIME type to associate with the file content in the `Content-Type` HTTP header.
|
||||
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
|
||||
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
|
||||
|
||||
//============================================================
|
||||
// Check 1 - is file URL?
|
||||
//============================================================
|
||||
|
||||
guard fileURL.isFileURL else {
|
||||
setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
|
||||
return
|
||||
}
|
||||
|
||||
//============================================================
|
||||
// Check 2 - is file URL reachable?
|
||||
//============================================================
|
||||
|
||||
do {
|
||||
let isReachable = try fileURL.checkPromisedItemIsReachable()
|
||||
guard isReachable else {
|
||||
setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
|
||||
return
|
||||
}
|
||||
|
||||
//============================================================
|
||||
// Check 3 - is file URL a directory?
|
||||
//============================================================
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
let path = fileURL.path
|
||||
|
||||
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else {
|
||||
setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
|
||||
return
|
||||
}
|
||||
|
||||
//============================================================
|
||||
// Check 4 - can the file size be extracted?
|
||||
//============================================================
|
||||
|
||||
let bodyContentLength: UInt64
|
||||
|
||||
do {
|
||||
guard let fileSize = try FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber else {
|
||||
setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
|
||||
return
|
||||
}
|
||||
|
||||
bodyContentLength = fileSize.uint64Value
|
||||
}
|
||||
catch {
|
||||
setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
|
||||
return
|
||||
}
|
||||
|
||||
//============================================================
|
||||
// Check 5 - can a stream be created from file URL?
|
||||
//============================================================
|
||||
|
||||
guard let stream = InputStream(url: fileURL) else {
|
||||
setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
|
||||
return
|
||||
}
|
||||
|
||||
append(stream, withLength: bodyContentLength, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates a body part from the stream and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
|
||||
/// - `Content-Type: #{mimeType}` (HTTP Header)
|
||||
/// - Encoded stream data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// - parameter stream: The input stream to encode in the multipart form data.
|
||||
/// - parameter length: The content length of the stream.
|
||||
/// - parameter name: The name to associate with the stream content in the `Content-Disposition` HTTP header.
|
||||
/// - parameter fileName: The filename to associate with the stream content in the `Content-Disposition` HTTP header.
|
||||
/// - parameter mimeType: The MIME type to associate with the stream content in the `Content-Type` HTTP header.
|
||||
public func append(
|
||||
_ stream: InputStream,
|
||||
withLength length: UInt64,
|
||||
name: String,
|
||||
fileName: String,
|
||||
mimeType: String)
|
||||
{
|
||||
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
|
||||
append(stream, withLength: length, headers: headers)
|
||||
}
|
||||
|
||||
/// Creates a body part with the headers, stream and length and appends it to the multipart form data object.
|
||||
///
|
||||
/// The body part data will be encoded using the following format:
|
||||
///
|
||||
/// - HTTP headers
|
||||
/// - Encoded stream data
|
||||
/// - Multipart form boundary
|
||||
///
|
||||
/// - parameter stream: The input stream to encode in the multipart form data.
|
||||
/// - parameter length: The content length of the stream.
|
||||
/// - parameter headers: The HTTP headers for the body part.
|
||||
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
|
||||
let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
|
||||
bodyParts.append(bodyPart)
|
||||
}
|
||||
|
||||
// MARK: - Data Encoding
|
||||
|
||||
/// Encodes all the appended body parts into a single `Data` value.
|
||||
///
|
||||
/// It is important to note that this method will load all the appended body parts into memory all at the same
|
||||
/// time. This method should only be used when the encoded data will have a small memory footprint. For large data
|
||||
/// cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method.
|
||||
///
|
||||
/// - throws: An `AFError` if encoding encounters an error.
|
||||
///
|
||||
/// - returns: The encoded `Data` if encoding is successful.
|
||||
public func encode() throws -> Data {
|
||||
if let bodyPartError = bodyPartError {
|
||||
throw bodyPartError
|
||||
}
|
||||
|
||||
var encoded = Data()
|
||||
|
||||
bodyParts.first?.hasInitialBoundary = true
|
||||
bodyParts.last?.hasFinalBoundary = true
|
||||
|
||||
for bodyPart in bodyParts {
|
||||
let encodedData = try encode(bodyPart)
|
||||
encoded.append(encodedData)
|
||||
}
|
||||
|
||||
return encoded
|
||||
}
|
||||
|
||||
/// Writes the appended body parts into the given file URL.
|
||||
///
|
||||
/// This process is facilitated by reading and writing with input and output streams, respectively. Thus,
|
||||
/// this approach is very memory efficient and should be used for large body part data.
|
||||
///
|
||||
/// - parameter fileURL: The file URL to write the multipart form data into.
|
||||
///
|
||||
/// - throws: An `AFError` if encoding encounters an error.
|
||||
public func writeEncodedData(to fileURL: URL) throws {
|
||||
if let bodyPartError = bodyPartError {
|
||||
throw bodyPartError
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
|
||||
} else if !fileURL.isFileURL {
|
||||
throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
|
||||
}
|
||||
|
||||
guard let outputStream = OutputStream(url: fileURL, append: false) else {
|
||||
throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
|
||||
}
|
||||
|
||||
outputStream.open()
|
||||
defer { outputStream.close() }
|
||||
|
||||
self.bodyParts.first?.hasInitialBoundary = true
|
||||
self.bodyParts.last?.hasFinalBoundary = true
|
||||
|
||||
for bodyPart in self.bodyParts {
|
||||
try write(bodyPart, to: outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private - Body Part Encoding
|
||||
|
||||
private func encode(_ bodyPart: BodyPart) throws -> Data {
|
||||
var encoded = Data()
|
||||
|
||||
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
|
||||
encoded.append(initialData)
|
||||
|
||||
let headerData = encodeHeaders(for: bodyPart)
|
||||
encoded.append(headerData)
|
||||
|
||||
let bodyStreamData = try encodeBodyStream(for: bodyPart)
|
||||
encoded.append(bodyStreamData)
|
||||
|
||||
if bodyPart.hasFinalBoundary {
|
||||
encoded.append(finalBoundaryData())
|
||||
}
|
||||
|
||||
return encoded
|
||||
}
|
||||
|
||||
private func encodeHeaders(for bodyPart: BodyPart) -> Data {
|
||||
var headerText = ""
|
||||
|
||||
for (key, value) in bodyPart.headers {
|
||||
headerText += "\(key): \(value)\(EncodingCharacters.crlf)"
|
||||
}
|
||||
headerText += EncodingCharacters.crlf
|
||||
|
||||
return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
|
||||
}
|
||||
|
||||
private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
|
||||
let inputStream = bodyPart.bodyStream
|
||||
inputStream.open()
|
||||
defer { inputStream.close() }
|
||||
|
||||
var encoded = Data()
|
||||
|
||||
while inputStream.hasBytesAvailable {
|
||||
var buffer = [UInt8](repeating: 0, count: streamBufferSize)
|
||||
let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
|
||||
|
||||
if let error = inputStream.streamError {
|
||||
throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
|
||||
}
|
||||
|
||||
if bytesRead > 0 {
|
||||
encoded.append(buffer, count: bytesRead)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return encoded
|
||||
}
|
||||
|
||||
// MARK: - Private - Writing Body Part to Output Stream
|
||||
|
||||
private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws {
|
||||
try writeInitialBoundaryData(for: bodyPart, to: outputStream)
|
||||
try writeHeaderData(for: bodyPart, to: outputStream)
|
||||
try writeBodyStream(for: bodyPart, to: outputStream)
|
||||
try writeFinalBoundaryData(for: bodyPart, to: outputStream)
|
||||
}
|
||||
|
||||
private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
|
||||
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
|
||||
return try write(initialData, to: outputStream)
|
||||
}
|
||||
|
||||
private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
|
||||
let headerData = encodeHeaders(for: bodyPart)
|
||||
return try write(headerData, to: outputStream)
|
||||
}
|
||||
|
||||
private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
|
||||
let inputStream = bodyPart.bodyStream
|
||||
|
||||
inputStream.open()
|
||||
defer { inputStream.close() }
|
||||
|
||||
while inputStream.hasBytesAvailable {
|
||||
var buffer = [UInt8](repeating: 0, count: streamBufferSize)
|
||||
let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
|
||||
|
||||
if let streamError = inputStream.streamError {
|
||||
throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError))
|
||||
}
|
||||
|
||||
if bytesRead > 0 {
|
||||
if buffer.count != bytesRead {
|
||||
buffer = Array(buffer[0..<bytesRead])
|
||||
}
|
||||
|
||||
try write(&buffer, to: outputStream)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
|
||||
if bodyPart.hasFinalBoundary {
|
||||
return try write(finalBoundaryData(), to: outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private - Writing Buffered Data to Output Stream
|
||||
|
||||
private func write(_ data: Data, to outputStream: OutputStream) throws {
|
||||
var buffer = [UInt8](repeating: 0, count: data.count)
|
||||
data.copyBytes(to: &buffer, count: data.count)
|
||||
|
||||
return try write(&buffer, to: outputStream)
|
||||
}
|
||||
|
||||
private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
|
||||
var bytesToWrite = buffer.count
|
||||
|
||||
while bytesToWrite > 0, outputStream.hasSpaceAvailable {
|
||||
let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
|
||||
|
||||
if let error = outputStream.streamError {
|
||||
throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error))
|
||||
}
|
||||
|
||||
bytesToWrite -= bytesWritten
|
||||
|
||||
if bytesToWrite > 0 {
|
||||
buffer = Array(buffer[bytesWritten..<buffer.count])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private - Mime Type
|
||||
|
||||
private func mimeType(forPathExtension pathExtension: String) -> String {
|
||||
if
|
||||
let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
|
||||
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue()
|
||||
{
|
||||
return contentType as String
|
||||
}
|
||||
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// MARK: - Private - Content Headers
|
||||
|
||||
private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] {
|
||||
var disposition = "form-data; name=\"\(name)\""
|
||||
if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" }
|
||||
|
||||
var headers = ["Content-Disposition": disposition]
|
||||
if let mimeType = mimeType { headers["Content-Type"] = mimeType }
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// MARK: - Private - Boundary Encoding
|
||||
|
||||
private func initialBoundaryData() -> Data {
|
||||
return BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary)
|
||||
}
|
||||
|
||||
private func encapsulatedBoundaryData() -> Data {
|
||||
return BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary)
|
||||
}
|
||||
|
||||
private func finalBoundaryData() -> Data {
|
||||
return BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary)
|
||||
}
|
||||
|
||||
// MARK: - Private - Errors
|
||||
|
||||
private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) {
|
||||
guard bodyPartError == nil else { return }
|
||||
bodyPartError = AFError.multipartEncodingFailed(reason: reason)
|
||||
}
|
||||
}
|
||||
233
Pods/Alamofire/Source/NetworkReachabilityManager.swift
generated
Normal file
233
Pods/Alamofire/Source/NetworkReachabilityManager.swift
generated
Normal file
@ -0,0 +1,233 @@
|
||||
//
|
||||
// NetworkReachabilityManager.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
import Foundation
|
||||
import SystemConfiguration
|
||||
|
||||
/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both WWAN and
|
||||
/// WiFi network interfaces.
|
||||
///
|
||||
/// Reachability can be used to determine background information about why a network operation failed, or to retry
|
||||
/// network requests when a connection is established. It should not be used to prevent a user from initiating a network
|
||||
/// request, as it's possible that an initial request may be required to establish reachability.
|
||||
open class NetworkReachabilityManager {
|
||||
/// Defines the various states of network reachability.
|
||||
///
|
||||
/// - unknown: It is unknown whether the network is reachable.
|
||||
/// - notReachable: The network is not reachable.
|
||||
/// - reachable: The network is reachable.
|
||||
public enum NetworkReachabilityStatus {
|
||||
case unknown
|
||||
case notReachable
|
||||
case reachable(ConnectionType)
|
||||
}
|
||||
|
||||
/// Defines the various connection types detected by reachability flags.
|
||||
///
|
||||
/// - ethernetOrWiFi: The connection type is either over Ethernet or WiFi.
|
||||
/// - wwan: The connection type is a WWAN connection.
|
||||
public enum ConnectionType {
|
||||
case ethernetOrWiFi
|
||||
case wwan
|
||||
}
|
||||
|
||||
/// A closure executed when the network reachability status changes. The closure takes a single argument: the
|
||||
/// network reachability status.
|
||||
public typealias Listener = (NetworkReachabilityStatus) -> Void
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Whether the network is currently reachable.
|
||||
open var isReachable: Bool { return isReachableOnWWAN || isReachableOnEthernetOrWiFi }
|
||||
|
||||
/// Whether the network is currently reachable over the WWAN interface.
|
||||
open var isReachableOnWWAN: Bool { return networkReachabilityStatus == .reachable(.wwan) }
|
||||
|
||||
/// Whether the network is currently reachable over Ethernet or WiFi interface.
|
||||
open var isReachableOnEthernetOrWiFi: Bool { return networkReachabilityStatus == .reachable(.ethernetOrWiFi) }
|
||||
|
||||
/// The current network reachability status.
|
||||
open var networkReachabilityStatus: NetworkReachabilityStatus {
|
||||
guard let flags = self.flags else { return .unknown }
|
||||
return networkReachabilityStatusForFlags(flags)
|
||||
}
|
||||
|
||||
/// The dispatch queue to execute the `listener` closure on.
|
||||
open var listenerQueue: DispatchQueue = DispatchQueue.main
|
||||
|
||||
/// A closure executed when the network reachability status changes.
|
||||
open var listener: Listener?
|
||||
|
||||
open var flags: SCNetworkReachabilityFlags? {
|
||||
var flags = SCNetworkReachabilityFlags()
|
||||
|
||||
if SCNetworkReachabilityGetFlags(reachability, &flags) {
|
||||
return flags
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private let reachability: SCNetworkReachability
|
||||
open var previousFlags: SCNetworkReachabilityFlags
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Creates a `NetworkReachabilityManager` instance with the specified host.
|
||||
///
|
||||
/// - parameter host: The host used to evaluate network reachability.
|
||||
///
|
||||
/// - returns: The new `NetworkReachabilityManager` instance.
|
||||
public convenience init?(host: String) {
|
||||
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
|
||||
self.init(reachability: reachability)
|
||||
}
|
||||
|
||||
/// Creates a `NetworkReachabilityManager` instance that monitors the address 0.0.0.0.
|
||||
///
|
||||
/// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
|
||||
/// status of the device, both IPv4 and IPv6.
|
||||
///
|
||||
/// - returns: The new `NetworkReachabilityManager` instance.
|
||||
public convenience init?() {
|
||||
var address = sockaddr_in()
|
||||
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
||||
address.sin_family = sa_family_t(AF_INET)
|
||||
|
||||
guard let reachability = withUnsafePointer(to: &address, { pointer in
|
||||
return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) {
|
||||
return SCNetworkReachabilityCreateWithAddress(nil, $0)
|
||||
}
|
||||
}) else { return nil }
|
||||
|
||||
self.init(reachability: reachability)
|
||||
}
|
||||
|
||||
private init(reachability: SCNetworkReachability) {
|
||||
self.reachability = reachability
|
||||
self.previousFlags = SCNetworkReachabilityFlags()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopListening()
|
||||
}
|
||||
|
||||
// MARK: - Listening
|
||||
|
||||
/// Starts listening for changes in network reachability status.
|
||||
///
|
||||
/// - returns: `true` if listening was started successfully, `false` otherwise.
|
||||
@discardableResult
|
||||
open func startListening() -> Bool {
|
||||
var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
|
||||
context.info = Unmanaged.passUnretained(self).toOpaque()
|
||||
|
||||
let callbackEnabled = SCNetworkReachabilitySetCallback(
|
||||
reachability,
|
||||
{ (_, flags, info) in
|
||||
let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
|
||||
reachability.notifyListener(flags)
|
||||
},
|
||||
&context
|
||||
)
|
||||
|
||||
let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)
|
||||
|
||||
listenerQueue.async {
|
||||
self.previousFlags = SCNetworkReachabilityFlags()
|
||||
self.notifyListener(self.flags ?? SCNetworkReachabilityFlags())
|
||||
}
|
||||
|
||||
return callbackEnabled && queueEnabled
|
||||
}
|
||||
|
||||
/// Stops listening for changes in network reachability status.
|
||||
open func stopListening() {
|
||||
SCNetworkReachabilitySetCallback(reachability, nil, nil)
|
||||
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
|
||||
}
|
||||
|
||||
// MARK: - Internal - Listener Notification
|
||||
|
||||
func notifyListener(_ flags: SCNetworkReachabilityFlags) {
|
||||
guard previousFlags != flags else { return }
|
||||
previousFlags = flags
|
||||
|
||||
listener?(networkReachabilityStatusForFlags(flags))
|
||||
}
|
||||
|
||||
// MARK: - Internal - Network Reachability Status
|
||||
|
||||
func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
|
||||
guard isNetworkReachable(with: flags) else { return .notReachable }
|
||||
|
||||
var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
|
||||
|
||||
#if os(iOS)
|
||||
if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
|
||||
#endif
|
||||
|
||||
return networkStatus
|
||||
}
|
||||
|
||||
func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
let isReachable = flags.contains(.reachable)
|
||||
let needsConnection = flags.contains(.connectionRequired)
|
||||
let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
|
||||
let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired)
|
||||
|
||||
return isReachable && (!needsConnection || canConnectWithoutUserInteraction)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {}
|
||||
|
||||
/// Returns whether the two network reachability status values are equal.
|
||||
///
|
||||
/// - parameter lhs: The left-hand side value to compare.
|
||||
/// - parameter rhs: The right-hand side value to compare.
|
||||
///
|
||||
/// - returns: `true` if the two values are equal, `false` otherwise.
|
||||
public func ==(
|
||||
lhs: NetworkReachabilityManager.NetworkReachabilityStatus,
|
||||
rhs: NetworkReachabilityManager.NetworkReachabilityStatus)
|
||||
-> Bool
|
||||
{
|
||||
switch (lhs, rhs) {
|
||||
case (.unknown, .unknown):
|
||||
return true
|
||||
case (.notReachable, .notReachable):
|
||||
return true
|
||||
case let (.reachable(lhsConnectionType), .reachable(rhsConnectionType)):
|
||||
return lhsConnectionType == rhsConnectionType
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
55
Pods/Alamofire/Source/Notifications.swift
generated
Normal file
55
Pods/Alamofire/Source/Notifications.swift
generated
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// Notifications.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Notification.Name {
|
||||
/// Used as a namespace for all `URLSessionTask` related notifications.
|
||||
public struct Task {
|
||||
/// Posted when a `URLSessionTask` is resumed. The notification `object` contains the resumed `URLSessionTask`.
|
||||
public static let DidResume = Notification.Name(rawValue: "org.alamofire.notification.name.task.didResume")
|
||||
|
||||
/// Posted when a `URLSessionTask` is suspended. The notification `object` contains the suspended `URLSessionTask`.
|
||||
public static let DidSuspend = Notification.Name(rawValue: "org.alamofire.notification.name.task.didSuspend")
|
||||
|
||||
/// Posted when a `URLSessionTask` is cancelled. The notification `object` contains the cancelled `URLSessionTask`.
|
||||
public static let DidCancel = Notification.Name(rawValue: "org.alamofire.notification.name.task.didCancel")
|
||||
|
||||
/// Posted when a `URLSessionTask` is completed. The notification `object` contains the completed `URLSessionTask`.
|
||||
public static let DidComplete = Notification.Name(rawValue: "org.alamofire.notification.name.task.didComplete")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension Notification {
|
||||
/// Used as a namespace for all `Notification` user info dictionary keys.
|
||||
public struct Key {
|
||||
/// User info dictionary key representing the `URLSessionTask` associated with the notification.
|
||||
public static let Task = "org.alamofire.notification.key.task"
|
||||
|
||||
/// User info dictionary key representing the responseData associated with the notification.
|
||||
public static let ResponseData = "org.alamofire.notification.key.responseData"
|
||||
}
|
||||
}
|
||||
483
Pods/Alamofire/Source/ParameterEncoding.swift
generated
Normal file
483
Pods/Alamofire/Source/ParameterEncoding.swift
generated
Normal file
@ -0,0 +1,483 @@
|
||||
//
|
||||
// ParameterEncoding.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// HTTP method definitions.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc7231#section-4.3
|
||||
public enum HTTPMethod: String {
|
||||
case options = "OPTIONS"
|
||||
case get = "GET"
|
||||
case head = "HEAD"
|
||||
case post = "POST"
|
||||
case put = "PUT"
|
||||
case patch = "PATCH"
|
||||
case delete = "DELETE"
|
||||
case trace = "TRACE"
|
||||
case connect = "CONNECT"
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// A dictionary of parameters to apply to a `URLRequest`.
|
||||
public typealias Parameters = [String: Any]
|
||||
|
||||
/// A type used to define how a set of parameters are applied to a `URLRequest`.
|
||||
public protocol ParameterEncoding {
|
||||
/// Creates a URL request by encoding parameters and applying them onto an existing request.
|
||||
///
|
||||
/// - parameter urlRequest: The request to have parameters applied.
|
||||
/// - parameter parameters: The parameters to apply.
|
||||
///
|
||||
/// - throws: An `AFError.parameterEncodingFailed` error if encoding fails.
|
||||
///
|
||||
/// - returns: The encoded request.
|
||||
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
|
||||
/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
|
||||
/// the HTTP body depends on the destination of the encoding.
|
||||
///
|
||||
/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
|
||||
/// `application/x-www-form-urlencoded; charset=utf-8`.
|
||||
///
|
||||
/// There is no published specification for how to encode collection types. By default the convention of appending
|
||||
/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for
|
||||
/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the
|
||||
/// square brackets appended to array keys.
|
||||
///
|
||||
/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
|
||||
/// `true` as 1 and `false` as 0.
|
||||
public struct URLEncoding: ParameterEncoding {
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
/// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the
|
||||
/// resulting URL request.
|
||||
///
|
||||
/// - methodDependent: Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE`
|
||||
/// requests and sets as the HTTP body for requests with any other HTTP method.
|
||||
/// - queryString: Sets or appends encoded query string result to existing query string.
|
||||
/// - httpBody: Sets encoded query string result as the HTTP body of the URL request.
|
||||
public enum Destination {
|
||||
case methodDependent, queryString, httpBody
|
||||
}
|
||||
|
||||
/// Configures how `Array` parameters are encoded.
|
||||
///
|
||||
/// - brackets: An empty set of square brackets is appended to the key for every value.
|
||||
/// This is the default behavior.
|
||||
/// - noBrackets: No brackets are appended. The key is encoded as is.
|
||||
public enum ArrayEncoding {
|
||||
case brackets, noBrackets
|
||||
|
||||
func encode(key: String) -> String {
|
||||
switch self {
|
||||
case .brackets:
|
||||
return "\(key)[]"
|
||||
case .noBrackets:
|
||||
return key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures how `Bool` parameters are encoded.
|
||||
///
|
||||
/// - numeric: Encode `true` as `1` and `false` as `0`. This is the default behavior.
|
||||
/// - literal: Encode `true` and `false` as string literals.
|
||||
public enum BoolEncoding {
|
||||
case numeric, literal
|
||||
|
||||
func encode(value: Bool) -> String {
|
||||
switch self {
|
||||
case .numeric:
|
||||
return value ? "1" : "0"
|
||||
case .literal:
|
||||
return value ? "true" : "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Returns a default `URLEncoding` instance.
|
||||
public static var `default`: URLEncoding { return URLEncoding() }
|
||||
|
||||
/// Returns a `URLEncoding` instance with a `.methodDependent` destination.
|
||||
public static var methodDependent: URLEncoding { return URLEncoding() }
|
||||
|
||||
/// Returns a `URLEncoding` instance with a `.queryString` destination.
|
||||
public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
|
||||
|
||||
/// Returns a `URLEncoding` instance with an `.httpBody` destination.
|
||||
public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
|
||||
|
||||
/// The destination defining where the encoded query string is to be applied to the URL request.
|
||||
public let destination: Destination
|
||||
|
||||
/// The encoding to use for `Array` parameters.
|
||||
public let arrayEncoding: ArrayEncoding
|
||||
|
||||
/// The encoding to use for `Bool` parameters.
|
||||
public let boolEncoding: BoolEncoding
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a `URLEncoding` instance using the specified destination.
|
||||
///
|
||||
/// - parameter destination: The destination defining where the encoded query string is to be applied.
|
||||
/// - parameter arrayEncoding: The encoding to use for `Array` parameters.
|
||||
/// - parameter boolEncoding: The encoding to use for `Bool` parameters.
|
||||
///
|
||||
/// - returns: The new `URLEncoding` instance.
|
||||
public init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) {
|
||||
self.destination = destination
|
||||
self.arrayEncoding = arrayEncoding
|
||||
self.boolEncoding = boolEncoding
|
||||
}
|
||||
|
||||
// MARK: Encoding
|
||||
|
||||
/// Creates a URL request by encoding parameters and applying them onto an existing request.
|
||||
///
|
||||
/// - parameter urlRequest: The request to have parameters applied.
|
||||
/// - parameter parameters: The parameters to apply.
|
||||
///
|
||||
/// - throws: An `Error` if the encoding process encounters an error.
|
||||
///
|
||||
/// - returns: The encoded request.
|
||||
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
|
||||
var urlRequest = try urlRequest.asURLRequest()
|
||||
|
||||
guard let parameters = parameters else { return urlRequest }
|
||||
|
||||
if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
|
||||
guard let url = urlRequest.url else {
|
||||
throw AFError.parameterEncodingFailed(reason: .missingURL)
|
||||
}
|
||||
|
||||
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
|
||||
let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
|
||||
urlComponents.percentEncodedQuery = percentEncodedQuery
|
||||
urlRequest.url = urlComponents.url
|
||||
}
|
||||
} else {
|
||||
if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
|
||||
urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
|
||||
/// Creates percent-escaped, URL encoded query string components from the given key-value pair using recursion.
|
||||
///
|
||||
/// - parameter key: The key of the query component.
|
||||
/// - parameter value: The value of the query component.
|
||||
///
|
||||
/// - returns: The percent-escaped, URL encoded query string components.
|
||||
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
|
||||
var components: [(String, String)] = []
|
||||
|
||||
if let dictionary = value as? [String: Any] {
|
||||
for (nestedKey, value) in dictionary {
|
||||
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
|
||||
}
|
||||
} else if let array = value as? [Any] {
|
||||
for value in array {
|
||||
components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
|
||||
}
|
||||
} else if let value = value as? NSNumber {
|
||||
if value.isBool {
|
||||
components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
|
||||
} else {
|
||||
components.append((escape(key), escape("\(value)")))
|
||||
}
|
||||
} else if let bool = value as? Bool {
|
||||
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
|
||||
} else {
|
||||
components.append((escape(key), escape("\(value)")))
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
/// Returns a percent-escaped string following RFC 3986 for a query string key or value.
|
||||
///
|
||||
/// RFC 3986 states that the following characters are "reserved" characters.
|
||||
///
|
||||
/// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
|
||||
/// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
|
||||
///
|
||||
/// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
|
||||
/// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
|
||||
/// should be percent-escaped in the query string.
|
||||
///
|
||||
/// - parameter string: The string to be percent-escaped.
|
||||
///
|
||||
/// - returns: The percent-escaped string.
|
||||
public func escape(_ string: String) -> String {
|
||||
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
|
||||
let subDelimitersToEncode = "!$&'()*+,;="
|
||||
|
||||
var allowedCharacterSet = CharacterSet.urlQueryAllowed
|
||||
allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
|
||||
|
||||
var escaped = ""
|
||||
|
||||
//==========================================================================================================
|
||||
//
|
||||
// Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few
|
||||
// hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no
|
||||
// longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more
|
||||
// info, please refer to:
|
||||
//
|
||||
// - https://github.com/Alamofire/Alamofire/issues/206
|
||||
//
|
||||
//==========================================================================================================
|
||||
|
||||
if #available(iOS 8.3, *) {
|
||||
escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
|
||||
} else {
|
||||
let batchSize = 50
|
||||
var index = string.startIndex
|
||||
|
||||
while index != string.endIndex {
|
||||
let startIndex = index
|
||||
let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex
|
||||
let range = startIndex..<endIndex
|
||||
|
||||
let substring = string[range]
|
||||
|
||||
escaped += substring.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? String(substring)
|
||||
|
||||
index = endIndex
|
||||
}
|
||||
}
|
||||
|
||||
return escaped
|
||||
}
|
||||
|
||||
private func query(_ parameters: [String: Any]) -> String {
|
||||
var components: [(String, String)] = []
|
||||
|
||||
for key in parameters.keys.sorted(by: <) {
|
||||
let value = parameters[key]!
|
||||
components += queryComponents(fromKey: key, value: value)
|
||||
}
|
||||
return components.map { "\($0)=\($1)" }.joined(separator: "&")
|
||||
}
|
||||
|
||||
private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
|
||||
switch destination {
|
||||
case .queryString:
|
||||
return true
|
||||
case .httpBody:
|
||||
return false
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch method {
|
||||
case .get, .head, .delete:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
|
||||
/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
|
||||
public struct JSONEncoding: ParameterEncoding {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Returns a `JSONEncoding` instance with default writing options.
|
||||
public static var `default`: JSONEncoding { return JSONEncoding() }
|
||||
|
||||
/// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options.
|
||||
public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }
|
||||
|
||||
/// The options for writing the parameters as JSON data.
|
||||
public let options: JSONSerialization.WritingOptions
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a `JSONEncoding` instance using the specified options.
|
||||
///
|
||||
/// - parameter options: The options for writing the parameters as JSON data.
|
||||
///
|
||||
/// - returns: The new `JSONEncoding` instance.
|
||||
public init(options: JSONSerialization.WritingOptions = []) {
|
||||
self.options = options
|
||||
}
|
||||
|
||||
// MARK: Encoding
|
||||
|
||||
/// Creates a URL request by encoding parameters and applying them onto an existing request.
|
||||
///
|
||||
/// - parameter urlRequest: The request to have parameters applied.
|
||||
/// - parameter parameters: The parameters to apply.
|
||||
///
|
||||
/// - throws: An `Error` if the encoding process encounters an error.
|
||||
///
|
||||
/// - returns: The encoded request.
|
||||
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
|
||||
var urlRequest = try urlRequest.asURLRequest()
|
||||
|
||||
guard let parameters = parameters else { return urlRequest }
|
||||
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
|
||||
|
||||
if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
|
||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
urlRequest.httpBody = data
|
||||
} catch {
|
||||
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
|
||||
/// Creates a URL request by encoding the JSON object and setting the resulting data on the HTTP body.
|
||||
///
|
||||
/// - parameter urlRequest: The request to apply the JSON object to.
|
||||
/// - parameter jsonObject: The JSON object to apply to the request.
|
||||
///
|
||||
/// - throws: An `Error` if the encoding process encounters an error.
|
||||
///
|
||||
/// - returns: The encoded request.
|
||||
public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
|
||||
var urlRequest = try urlRequest.asURLRequest()
|
||||
|
||||
guard let jsonObject = jsonObject else { return urlRequest }
|
||||
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)
|
||||
|
||||
if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
|
||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
urlRequest.httpBody = data
|
||||
} catch {
|
||||
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Uses `PropertyListSerialization` to create a plist representation of the parameters object, according to the
|
||||
/// associated format and write options values, which is set as the body of the request. The `Content-Type` HTTP header
|
||||
/// field of an encoded request is set to `application/x-plist`.
|
||||
public struct PropertyListEncoding: ParameterEncoding {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Returns a default `PropertyListEncoding` instance.
|
||||
public static var `default`: PropertyListEncoding { return PropertyListEncoding() }
|
||||
|
||||
/// Returns a `PropertyListEncoding` instance with xml formatting and default writing options.
|
||||
public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) }
|
||||
|
||||
/// Returns a `PropertyListEncoding` instance with binary formatting and default writing options.
|
||||
public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) }
|
||||
|
||||
/// The property list serialization format.
|
||||
public let format: PropertyListSerialization.PropertyListFormat
|
||||
|
||||
/// The options for writing the parameters as plist data.
|
||||
public let options: PropertyListSerialization.WriteOptions
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a `PropertyListEncoding` instance using the specified format and options.
|
||||
///
|
||||
/// - parameter format: The property list serialization format.
|
||||
/// - parameter options: The options for writing the parameters as plist data.
|
||||
///
|
||||
/// - returns: The new `PropertyListEncoding` instance.
|
||||
public init(
|
||||
format: PropertyListSerialization.PropertyListFormat = .xml,
|
||||
options: PropertyListSerialization.WriteOptions = 0)
|
||||
{
|
||||
self.format = format
|
||||
self.options = options
|
||||
}
|
||||
|
||||
// MARK: Encoding
|
||||
|
||||
/// Creates a URL request by encoding parameters and applying them onto an existing request.
|
||||
///
|
||||
/// - parameter urlRequest: The request to have parameters applied.
|
||||
/// - parameter parameters: The parameters to apply.
|
||||
///
|
||||
/// - throws: An `Error` if the encoding process encounters an error.
|
||||
///
|
||||
/// - returns: The encoded request.
|
||||
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
|
||||
var urlRequest = try urlRequest.asURLRequest()
|
||||
|
||||
guard let parameters = parameters else { return urlRequest }
|
||||
|
||||
do {
|
||||
let data = try PropertyListSerialization.data(
|
||||
fromPropertyList: parameters,
|
||||
format: format,
|
||||
options: options
|
||||
)
|
||||
|
||||
if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
|
||||
urlRequest.setValue("application/x-plist", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
urlRequest.httpBody = data
|
||||
} catch {
|
||||
throw AFError.parameterEncodingFailed(reason: .propertyListEncodingFailed(error: error))
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension NSNumber {
|
||||
fileprivate var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) }
|
||||
}
|
||||
654
Pods/Alamofire/Source/Request.swift
generated
Normal file
654
Pods/Alamofire/Source/Request.swift
generated
Normal file
@ -0,0 +1,654 @@
|
||||
//
|
||||
// Request.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary.
|
||||
public protocol RequestAdapter {
|
||||
/// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request to adapt.
|
||||
///
|
||||
/// - throws: An `Error` if the adaptation encounters an error.
|
||||
///
|
||||
/// - returns: The adapted `URLRequest`.
|
||||
func adapt(_ urlRequest: URLRequest) throws -> URLRequest
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// A closure executed when the `RequestRetrier` determines whether a `Request` should be retried or not.
|
||||
public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void
|
||||
|
||||
/// A type that determines whether a request should be retried after being executed by the specified session manager
|
||||
/// and encountering an error.
|
||||
public protocol RequestRetrier {
|
||||
/// Determines whether the `Request` should be retried by calling the `completion` closure.
|
||||
///
|
||||
/// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs
|
||||
/// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
|
||||
/// cleaned up after.
|
||||
///
|
||||
/// - parameter manager: The session manager the request was executed on.
|
||||
/// - parameter request: The request that failed due to the encountered error.
|
||||
/// - parameter error: The error encountered when executing the request.
|
||||
/// - parameter completion: The completion closure to be executed when retry decision has been determined.
|
||||
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
protocol TaskConvertible {
|
||||
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask
|
||||
}
|
||||
|
||||
/// A dictionary of headers to apply to a `URLRequest`.
|
||||
public typealias HTTPHeaders = [String: String]
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Responsible for sending a request and receiving the response and associated data from the server, as well as
|
||||
/// managing its underlying `URLSessionTask`.
|
||||
open class Request {
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
/// A closure executed when monitoring upload or download progress of a request.
|
||||
public typealias ProgressHandler = (Progress) -> Void
|
||||
|
||||
enum RequestTask {
|
||||
case data(TaskConvertible?, URLSessionTask?)
|
||||
case download(TaskConvertible?, URLSessionTask?)
|
||||
case upload(TaskConvertible?, URLSessionTask?)
|
||||
case stream(TaskConvertible?, URLSessionTask?)
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The delegate for the underlying task.
|
||||
open internal(set) var delegate: TaskDelegate {
|
||||
get {
|
||||
taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
|
||||
return taskDelegate
|
||||
}
|
||||
set {
|
||||
taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
|
||||
taskDelegate = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The underlying task.
|
||||
open var task: URLSessionTask? { return delegate.task }
|
||||
|
||||
/// The session belonging to the underlying task.
|
||||
public let session: URLSession
|
||||
|
||||
/// The request sent or to be sent to the server.
|
||||
open var request: URLRequest? { return task?.originalRequest }
|
||||
|
||||
/// The response received from the server, if any.
|
||||
open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse }
|
||||
|
||||
/// The number of times the request has been retried.
|
||||
open internal(set) var retryCount: UInt = 0
|
||||
|
||||
let originalTask: TaskConvertible?
|
||||
|
||||
var startTime: CFAbsoluteTime?
|
||||
var endTime: CFAbsoluteTime?
|
||||
|
||||
var validations: [() -> Void] = []
|
||||
|
||||
private var taskDelegate: TaskDelegate
|
||||
private var taskDelegateLock = NSLock()
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
|
||||
self.session = session
|
||||
|
||||
switch requestTask {
|
||||
case .data(let originalTask, let task):
|
||||
taskDelegate = DataTaskDelegate(task: task)
|
||||
self.originalTask = originalTask
|
||||
case .download(let originalTask, let task):
|
||||
taskDelegate = DownloadTaskDelegate(task: task)
|
||||
self.originalTask = originalTask
|
||||
case .upload(let originalTask, let task):
|
||||
taskDelegate = UploadTaskDelegate(task: task)
|
||||
self.originalTask = originalTask
|
||||
case .stream(let originalTask, let task):
|
||||
taskDelegate = TaskDelegate(task: task)
|
||||
self.originalTask = originalTask
|
||||
}
|
||||
|
||||
delegate.error = error
|
||||
delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
|
||||
}
|
||||
|
||||
// MARK: Authentication
|
||||
|
||||
/// Associates an HTTP Basic credential with the request.
|
||||
///
|
||||
/// - parameter user: The user.
|
||||
/// - parameter password: The password.
|
||||
/// - parameter persistence: The URL credential persistence. `.ForSession` by default.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
open func authenticate(
|
||||
user: String,
|
||||
password: String,
|
||||
persistence: URLCredential.Persistence = .forSession)
|
||||
-> Self
|
||||
{
|
||||
let credential = URLCredential(user: user, password: password, persistence: persistence)
|
||||
return authenticate(usingCredential: credential)
|
||||
}
|
||||
|
||||
/// Associates a specified credential with the request.
|
||||
///
|
||||
/// - parameter credential: The credential.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
open func authenticate(usingCredential credential: URLCredential) -> Self {
|
||||
delegate.credential = credential
|
||||
return self
|
||||
}
|
||||
|
||||
/// Returns a base64 encoded basic authentication credential as an authorization header tuple.
|
||||
///
|
||||
/// - parameter user: The user.
|
||||
/// - parameter password: The password.
|
||||
///
|
||||
/// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
|
||||
open class func authorizationHeader(user: String, password: String) -> (key: String, value: String)? {
|
||||
guard let data = "\(user):\(password)".data(using: .utf8) else { return nil }
|
||||
|
||||
let credential = data.base64EncodedString(options: [])
|
||||
|
||||
return (key: "Authorization", value: "Basic \(credential)")
|
||||
}
|
||||
|
||||
// MARK: State
|
||||
|
||||
/// Resumes the request.
|
||||
open func resume() {
|
||||
guard let task = task else { delegate.queue.isSuspended = false ; return }
|
||||
|
||||
if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
|
||||
|
||||
task.resume()
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name.Task.DidResume,
|
||||
object: self,
|
||||
userInfo: [Notification.Key.Task: task]
|
||||
)
|
||||
}
|
||||
|
||||
/// Suspends the request.
|
||||
open func suspend() {
|
||||
guard let task = task else { return }
|
||||
|
||||
task.suspend()
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name.Task.DidSuspend,
|
||||
object: self,
|
||||
userInfo: [Notification.Key.Task: task]
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancels the request.
|
||||
open func cancel() {
|
||||
guard let task = task else { return }
|
||||
|
||||
task.cancel()
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name.Task.DidCancel,
|
||||
object: self,
|
||||
userInfo: [Notification.Key.Task: task]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Request: CustomStringConvertible {
|
||||
/// The textual representation used when written to an output stream, which includes the HTTP method and URL, as
|
||||
/// well as the response status code if a response has been received.
|
||||
open var description: String {
|
||||
var components: [String] = []
|
||||
|
||||
if let HTTPMethod = request?.httpMethod {
|
||||
components.append(HTTPMethod)
|
||||
}
|
||||
|
||||
if let urlString = request?.url?.absoluteString {
|
||||
components.append(urlString)
|
||||
}
|
||||
|
||||
if let response = response {
|
||||
components.append("(\(response.statusCode))")
|
||||
}
|
||||
|
||||
return components.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomDebugStringConvertible
|
||||
|
||||
extension Request: CustomDebugStringConvertible {
|
||||
/// The textual representation used when written to an output stream, in the form of a cURL command.
|
||||
open var debugDescription: String {
|
||||
return cURLRepresentation()
|
||||
}
|
||||
|
||||
func cURLRepresentation() -> String {
|
||||
var components = ["$ curl -v"]
|
||||
|
||||
guard let request = self.request,
|
||||
let url = request.url,
|
||||
let host = url.host
|
||||
else {
|
||||
return "$ curl command could not be created"
|
||||
}
|
||||
|
||||
if let httpMethod = request.httpMethod, httpMethod != "GET" {
|
||||
components.append("-X \(httpMethod)")
|
||||
}
|
||||
|
||||
if let credentialStorage = self.session.configuration.urlCredentialStorage {
|
||||
let protectionSpace = URLProtectionSpace(
|
||||
host: host,
|
||||
port: url.port ?? 0,
|
||||
protocol: url.scheme,
|
||||
realm: host,
|
||||
authenticationMethod: NSURLAuthenticationMethodHTTPBasic
|
||||
)
|
||||
|
||||
if let credentials = credentialStorage.credentials(for: protectionSpace)?.values {
|
||||
for credential in credentials {
|
||||
guard let user = credential.user, let password = credential.password else { continue }
|
||||
components.append("-u \(user):\(password)")
|
||||
}
|
||||
} else {
|
||||
if let credential = delegate.credential, let user = credential.user, let password = credential.password {
|
||||
components.append("-u \(user):\(password)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if session.configuration.httpShouldSetCookies {
|
||||
if
|
||||
let cookieStorage = session.configuration.httpCookieStorage,
|
||||
let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty
|
||||
{
|
||||
let string = cookies.reduce("") { $0 + "\($1.name)=\($1.value);" }
|
||||
|
||||
#if swift(>=3.2)
|
||||
components.append("-b \"\(string[..<string.index(before: string.endIndex)])\"")
|
||||
#else
|
||||
components.append("-b \"\(string.substring(to: string.characters.index(before: string.endIndex)))\"")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [AnyHashable: Any] = [:]
|
||||
|
||||
if let additionalHeaders = session.configuration.httpAdditionalHeaders {
|
||||
for (field, value) in additionalHeaders where field != AnyHashable("Cookie") {
|
||||
headers[field] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let headerFields = request.allHTTPHeaderFields {
|
||||
for (field, value) in headerFields where field != "Cookie" {
|
||||
headers[field] = value
|
||||
}
|
||||
}
|
||||
|
||||
for (field, value) in headers {
|
||||
let escapedValue = String(describing: value).replacingOccurrences(of: "\"", with: "\\\"")
|
||||
components.append("-H \"\(field): \(escapedValue)\"")
|
||||
}
|
||||
|
||||
if let httpBodyData = request.httpBody, let httpBody = String(data: httpBodyData, encoding: .utf8) {
|
||||
var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"")
|
||||
escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
|
||||
components.append("-d \"\(escapedBody)\"")
|
||||
}
|
||||
|
||||
components.append("\"\(url.absoluteString)\"")
|
||||
|
||||
return components.joined(separator: " \\\n\t")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Specific type of `Request` that manages an underlying `URLSessionDataTask`.
|
||||
open class DataRequest: Request {
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
struct Requestable: TaskConvertible {
|
||||
let urlRequest: URLRequest
|
||||
|
||||
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
|
||||
do {
|
||||
let urlRequest = try self.urlRequest.adapt(using: adapter)
|
||||
return queue.sync { session.dataTask(with: urlRequest) }
|
||||
} catch {
|
||||
throw AdaptError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The request sent or to be sent to the server.
|
||||
open override var request: URLRequest? {
|
||||
if let request = super.request { return request }
|
||||
if let requestable = originalTask as? Requestable { return requestable.urlRequest }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The progress of fetching the response data from the server for the request.
|
||||
open var progress: Progress { return dataDelegate.progress }
|
||||
|
||||
var dataDelegate: DataTaskDelegate { return delegate as! DataTaskDelegate }
|
||||
|
||||
// MARK: Stream
|
||||
|
||||
/// Sets a closure to be called periodically during the lifecycle of the request as data is read from the server.
|
||||
///
|
||||
/// This closure returns the bytes most recently received from the server, not including data from previous calls.
|
||||
/// If this closure is set, data will only be available within this closure, and will not be saved elsewhere. It is
|
||||
/// also important to note that the server data in any `Response` object will be `nil`.
|
||||
///
|
||||
/// - parameter closure: The code to be executed periodically during the lifecycle of the request.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
open func stream(closure: ((Data) -> Void)? = nil) -> Self {
|
||||
dataDelegate.dataStream = closure
|
||||
return self
|
||||
}
|
||||
|
||||
// MARK: Progress
|
||||
|
||||
/// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server.
|
||||
///
|
||||
/// - parameter queue: The dispatch queue to execute the closure on.
|
||||
/// - parameter closure: The code to be executed periodically as data is read from the server.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
|
||||
dataDelegate.progressHandler = (closure, queue)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Specific type of `Request` that manages an underlying `URLSessionDownloadTask`.
|
||||
open class DownloadRequest: Request {
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
/// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
|
||||
/// destination URL.
|
||||
public struct DownloadOptions: OptionSet {
|
||||
/// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol.
|
||||
public let rawValue: UInt
|
||||
|
||||
/// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
|
||||
public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0)
|
||||
|
||||
/// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
|
||||
public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1)
|
||||
|
||||
/// Creates a `DownloadFileDestinationOptions` instance with the specified raw value.
|
||||
///
|
||||
/// - parameter rawValue: The raw bitmask value for the option.
|
||||
///
|
||||
/// - returns: A new log level instance.
|
||||
public init(rawValue: UInt) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// A closure executed once a download request has successfully completed in order to determine where to move the
|
||||
/// temporary file written to during the download process. The closure takes two arguments: the temporary file URL
|
||||
/// and the URL response, and returns a two arguments: the file URL where the temporary file should be moved and
|
||||
/// the options defining how the file should be moved.
|
||||
public typealias DownloadFileDestination = (
|
||||
_ temporaryURL: URL,
|
||||
_ response: HTTPURLResponse)
|
||||
-> (destinationURL: URL, options: DownloadOptions)
|
||||
|
||||
enum Downloadable: TaskConvertible {
|
||||
case request(URLRequest)
|
||||
case resumeData(Data)
|
||||
|
||||
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
|
||||
do {
|
||||
let task: URLSessionTask
|
||||
|
||||
switch self {
|
||||
case let .request(urlRequest):
|
||||
let urlRequest = try urlRequest.adapt(using: adapter)
|
||||
task = queue.sync { session.downloadTask(with: urlRequest) }
|
||||
case let .resumeData(resumeData):
|
||||
task = queue.sync { session.downloadTask(withResumeData: resumeData) }
|
||||
}
|
||||
|
||||
return task
|
||||
} catch {
|
||||
throw AdaptError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The request sent or to be sent to the server.
|
||||
open override var request: URLRequest? {
|
||||
if let request = super.request { return request }
|
||||
|
||||
if let downloadable = originalTask as? Downloadable, case let .request(urlRequest) = downloadable {
|
||||
return urlRequest
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The resume data of the underlying download task if available after a failure.
|
||||
open var resumeData: Data? { return downloadDelegate.resumeData }
|
||||
|
||||
/// The progress of downloading the response data from the server for the request.
|
||||
open var progress: Progress { return downloadDelegate.progress }
|
||||
|
||||
var downloadDelegate: DownloadTaskDelegate { return delegate as! DownloadTaskDelegate }
|
||||
|
||||
// MARK: State
|
||||
|
||||
/// Cancels the request.
|
||||
open override func cancel() {
|
||||
downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 }
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name.Task.DidCancel,
|
||||
object: self,
|
||||
userInfo: [Notification.Key.Task: task as Any]
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Progress
|
||||
|
||||
/// Sets a closure to be called periodically during the lifecycle of the `Request` as data is read from the server.
|
||||
///
|
||||
/// - parameter queue: The dispatch queue to execute the closure on.
|
||||
/// - parameter closure: The code to be executed periodically as data is read from the server.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
|
||||
downloadDelegate.progressHandler = (closure, queue)
|
||||
return self
|
||||
}
|
||||
|
||||
// MARK: Destination
|
||||
|
||||
/// Creates a download file destination closure which uses the default file manager to move the temporary file to a
|
||||
/// file URL in the first available directory with the specified search path directory and search path domain mask.
|
||||
///
|
||||
/// - parameter directory: The search path directory. `.DocumentDirectory` by default.
|
||||
/// - parameter domain: The search path domain mask. `.UserDomainMask` by default.
|
||||
///
|
||||
/// - returns: A download file destination closure.
|
||||
open class func suggestedDownloadDestination(
|
||||
for directory: FileManager.SearchPathDirectory = .documentDirectory,
|
||||
in domain: FileManager.SearchPathDomainMask = .userDomainMask)
|
||||
-> DownloadFileDestination
|
||||
{
|
||||
return { temporaryURL, response in
|
||||
let directoryURLs = FileManager.default.urls(for: directory, in: domain)
|
||||
|
||||
if !directoryURLs.isEmpty {
|
||||
return (directoryURLs[0].appendingPathComponent(response.suggestedFilename!), [])
|
||||
}
|
||||
|
||||
return (temporaryURL, [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Specific type of `Request` that manages an underlying `URLSessionUploadTask`.
|
||||
open class UploadRequest: DataRequest {
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
enum Uploadable: TaskConvertible {
|
||||
case data(Data, URLRequest)
|
||||
case file(URL, URLRequest)
|
||||
case stream(InputStream, URLRequest)
|
||||
|
||||
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
|
||||
do {
|
||||
let task: URLSessionTask
|
||||
|
||||
switch self {
|
||||
case let .data(data, urlRequest):
|
||||
let urlRequest = try urlRequest.adapt(using: adapter)
|
||||
task = queue.sync { session.uploadTask(with: urlRequest, from: data) }
|
||||
case let .file(url, urlRequest):
|
||||
let urlRequest = try urlRequest.adapt(using: adapter)
|
||||
task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
|
||||
case let .stream(_, urlRequest):
|
||||
let urlRequest = try urlRequest.adapt(using: adapter)
|
||||
task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
|
||||
}
|
||||
|
||||
return task
|
||||
} catch {
|
||||
throw AdaptError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The request sent or to be sent to the server.
|
||||
open override var request: URLRequest? {
|
||||
if let request = super.request { return request }
|
||||
|
||||
guard let uploadable = originalTask as? Uploadable else { return nil }
|
||||
|
||||
switch uploadable {
|
||||
case .data(_, let urlRequest), .file(_, let urlRequest), .stream(_, let urlRequest):
|
||||
return urlRequest
|
||||
}
|
||||
}
|
||||
|
||||
/// The progress of uploading the payload to the server for the upload request.
|
||||
open var uploadProgress: Progress { return uploadDelegate.uploadProgress }
|
||||
|
||||
var uploadDelegate: UploadTaskDelegate { return delegate as! UploadTaskDelegate }
|
||||
|
||||
// MARK: Upload Progress
|
||||
|
||||
/// Sets a closure to be called periodically during the lifecycle of the `UploadRequest` as data is sent to
|
||||
/// the server.
|
||||
///
|
||||
/// After the data is sent to the server, the `progress(queue:closure:)` APIs can be used to monitor the progress
|
||||
/// of data being read from the server.
|
||||
///
|
||||
/// - parameter queue: The dispatch queue to execute the closure on.
|
||||
/// - parameter closure: The code to be executed periodically as data is sent to the server.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
open func uploadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
|
||||
uploadDelegate.uploadProgressHandler = (closure, queue)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
/// Specific type of `Request` that manages an underlying `URLSessionStreamTask`.
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open class StreamRequest: Request {
|
||||
enum Streamable: TaskConvertible {
|
||||
case stream(hostName: String, port: Int)
|
||||
case netService(NetService)
|
||||
|
||||
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
|
||||
let task: URLSessionTask
|
||||
|
||||
switch self {
|
||||
case let .stream(hostName, port):
|
||||
task = queue.sync { session.streamTask(withHostName: hostName, port: port) }
|
||||
case let .netService(netService):
|
||||
task = queue.sync { session.streamTask(with: netService) }
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
567
Pods/Alamofire/Source/Response.swift
generated
Normal file
567
Pods/Alamofire/Source/Response.swift
generated
Normal file
@ -0,0 +1,567 @@
|
||||
//
|
||||
// Response.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Used to store all data associated with an non-serialized response of a data or upload request.
|
||||
public struct DefaultDataResponse {
|
||||
/// The URL request sent to the server.
|
||||
public let request: URLRequest?
|
||||
|
||||
/// The server's response to the URL request.
|
||||
public let response: HTTPURLResponse?
|
||||
|
||||
/// The data returned by the server.
|
||||
public let data: Data?
|
||||
|
||||
/// The error encountered while executing or validating the request.
|
||||
public let error: Error?
|
||||
|
||||
/// The timeline of the complete lifecycle of the request.
|
||||
public let timeline: Timeline
|
||||
|
||||
var _metrics: AnyObject?
|
||||
|
||||
/// Creates a `DefaultDataResponse` instance from the specified parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: The URL request sent to the server.
|
||||
/// - response: The server's response to the URL request.
|
||||
/// - data: The data returned by the server.
|
||||
/// - error: The error encountered while executing or validating the request.
|
||||
/// - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default.
|
||||
/// - metrics: The task metrics containing the request / response statistics. `nil` by default.
|
||||
public init(
|
||||
request: URLRequest?,
|
||||
response: HTTPURLResponse?,
|
||||
data: Data?,
|
||||
error: Error?,
|
||||
timeline: Timeline = Timeline(),
|
||||
metrics: AnyObject? = nil)
|
||||
{
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.data = data
|
||||
self.error = error
|
||||
self.timeline = timeline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Used to store all data associated with a serialized response of a data or upload request.
|
||||
public struct DataResponse<Value> {
|
||||
/// The URL request sent to the server.
|
||||
public let request: URLRequest?
|
||||
|
||||
/// The server's response to the URL request.
|
||||
public let response: HTTPURLResponse?
|
||||
|
||||
/// The data returned by the server.
|
||||
public let data: Data?
|
||||
|
||||
/// The result of response serialization.
|
||||
public let result: Result<Value>
|
||||
|
||||
/// The timeline of the complete lifecycle of the request.
|
||||
public let timeline: Timeline
|
||||
|
||||
/// Returns the associated value of the result if it is a success, `nil` otherwise.
|
||||
public var value: Value? { return result.value }
|
||||
|
||||
/// Returns the associated error value if the result if it is a failure, `nil` otherwise.
|
||||
public var error: Error? { return result.error }
|
||||
|
||||
var _metrics: AnyObject?
|
||||
|
||||
/// Creates a `DataResponse` instance with the specified parameters derived from response serialization.
|
||||
///
|
||||
/// - parameter request: The URL request sent to the server.
|
||||
/// - parameter response: The server's response to the URL request.
|
||||
/// - parameter data: The data returned by the server.
|
||||
/// - parameter result: The result of response serialization.
|
||||
/// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`.
|
||||
///
|
||||
/// - returns: The new `DataResponse` instance.
|
||||
public init(
|
||||
request: URLRequest?,
|
||||
response: HTTPURLResponse?,
|
||||
data: Data?,
|
||||
result: Result<Value>,
|
||||
timeline: Timeline = Timeline())
|
||||
{
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.data = data
|
||||
self.result = result
|
||||
self.timeline = timeline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
/// The textual representation used when written to an output stream, which includes whether the result was a
|
||||
/// success or failure.
|
||||
public var description: String {
|
||||
return result.debugDescription
|
||||
}
|
||||
|
||||
/// The debug textual representation used when written to an output stream, which includes the URL request, the URL
|
||||
/// response, the server data, the response serialization result and the timeline.
|
||||
public var debugDescription: String {
|
||||
var output: [String] = []
|
||||
|
||||
output.append(request != nil ? "[Request]: \(request!.httpMethod ?? "GET") \(request!)" : "[Request]: nil")
|
||||
output.append(response != nil ? "[Response]: \(response!)" : "[Response]: nil")
|
||||
output.append("[Data]: \(data?.count ?? 0) bytes")
|
||||
output.append("[Result]: \(result.debugDescription)")
|
||||
output.append("[Timeline]: \(timeline.debugDescription)")
|
||||
|
||||
return output.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension DataResponse {
|
||||
/// Evaluates the specified closure when the result of this `DataResponse` is a success, passing the unwrapped
|
||||
/// result value as a parameter.
|
||||
///
|
||||
/// Use the `map` method with a closure that does not throw. For example:
|
||||
///
|
||||
/// let possibleData: DataResponse<Data> = ...
|
||||
/// let possibleInt = possibleData.map { $0.count }
|
||||
///
|
||||
/// - parameter transform: A closure that takes the success value of the instance's result.
|
||||
///
|
||||
/// - returns: A `DataResponse` whose result wraps the value returned by the given closure. If this instance's
|
||||
/// result is a failure, returns a response wrapping the same failure.
|
||||
public func map<T>(_ transform: (Value) -> T) -> DataResponse<T> {
|
||||
var response = DataResponse<T>(
|
||||
request: request,
|
||||
response: self.response,
|
||||
data: data,
|
||||
result: result.map(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/// Evaluates the given closure when the result of this `DataResponse` is a success, passing the unwrapped result
|
||||
/// value as a parameter.
|
||||
///
|
||||
/// Use the `flatMap` method with a closure that may throw an error. For example:
|
||||
///
|
||||
/// let possibleData: DataResponse<Data> = ...
|
||||
/// let possibleObject = possibleData.flatMap {
|
||||
/// try JSONSerialization.jsonObject(with: $0)
|
||||
/// }
|
||||
///
|
||||
/// - parameter transform: A closure that takes the success value of the instance's result.
|
||||
///
|
||||
/// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's
|
||||
/// result is a failure, returns the same failure.
|
||||
public func flatMap<T>(_ transform: (Value) throws -> T) -> DataResponse<T> {
|
||||
var response = DataResponse<T>(
|
||||
request: request,
|
||||
response: self.response,
|
||||
data: data,
|
||||
result: result.flatMap(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `mapError` function with a closure that does not throw. For example:
|
||||
///
|
||||
/// let possibleData: DataResponse<Data> = ...
|
||||
/// let withMyError = possibleData.mapError { MyError.error($0) }
|
||||
///
|
||||
/// - Parameter transform: A closure that takes the error of the instance.
|
||||
/// - Returns: A `DataResponse` instance containing the result of the transform.
|
||||
public func mapError<E: Error>(_ transform: (Error) -> E) -> DataResponse {
|
||||
var response = DataResponse(
|
||||
request: request,
|
||||
response: self.response,
|
||||
data: data,
|
||||
result: result.mapError(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `flatMapError` function with a closure that may throw an error. For example:
|
||||
///
|
||||
/// let possibleData: DataResponse<Data> = ...
|
||||
/// let possibleObject = possibleData.flatMapError {
|
||||
/// try someFailableFunction(taking: $0)
|
||||
/// }
|
||||
///
|
||||
/// - Parameter transform: A throwing closure that takes the error of the instance.
|
||||
///
|
||||
/// - Returns: A `DataResponse` instance containing the result of the transform.
|
||||
public func flatMapError<E: Error>(_ transform: (Error) throws -> E) -> DataResponse {
|
||||
var response = DataResponse(
|
||||
request: request,
|
||||
response: self.response,
|
||||
data: data,
|
||||
result: result.flatMapError(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Used to store all data associated with an non-serialized response of a download request.
|
||||
public struct DefaultDownloadResponse {
|
||||
/// The URL request sent to the server.
|
||||
public let request: URLRequest?
|
||||
|
||||
/// The server's response to the URL request.
|
||||
public let response: HTTPURLResponse?
|
||||
|
||||
/// The temporary destination URL of the data returned from the server.
|
||||
public let temporaryURL: URL?
|
||||
|
||||
/// The final destination URL of the data returned from the server if it was moved.
|
||||
public let destinationURL: URL?
|
||||
|
||||
/// The resume data generated if the request was cancelled.
|
||||
public let resumeData: Data?
|
||||
|
||||
/// The error encountered while executing or validating the request.
|
||||
public let error: Error?
|
||||
|
||||
/// The timeline of the complete lifecycle of the request.
|
||||
public let timeline: Timeline
|
||||
|
||||
var _metrics: AnyObject?
|
||||
|
||||
/// Creates a `DefaultDownloadResponse` instance from the specified parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: The URL request sent to the server.
|
||||
/// - response: The server's response to the URL request.
|
||||
/// - temporaryURL: The temporary destination URL of the data returned from the server.
|
||||
/// - destinationURL: The final destination URL of the data returned from the server if it was moved.
|
||||
/// - resumeData: The resume data generated if the request was cancelled.
|
||||
/// - error: The error encountered while executing or validating the request.
|
||||
/// - timeline: The timeline of the complete lifecycle of the request. `Timeline()` by default.
|
||||
/// - metrics: The task metrics containing the request / response statistics. `nil` by default.
|
||||
public init(
|
||||
request: URLRequest?,
|
||||
response: HTTPURLResponse?,
|
||||
temporaryURL: URL?,
|
||||
destinationURL: URL?,
|
||||
resumeData: Data?,
|
||||
error: Error?,
|
||||
timeline: Timeline = Timeline(),
|
||||
metrics: AnyObject? = nil)
|
||||
{
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.temporaryURL = temporaryURL
|
||||
self.destinationURL = destinationURL
|
||||
self.resumeData = resumeData
|
||||
self.error = error
|
||||
self.timeline = timeline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Used to store all data associated with a serialized response of a download request.
|
||||
public struct DownloadResponse<Value> {
|
||||
/// The URL request sent to the server.
|
||||
public let request: URLRequest?
|
||||
|
||||
/// The server's response to the URL request.
|
||||
public let response: HTTPURLResponse?
|
||||
|
||||
/// The temporary destination URL of the data returned from the server.
|
||||
public let temporaryURL: URL?
|
||||
|
||||
/// The final destination URL of the data returned from the server if it was moved.
|
||||
public let destinationURL: URL?
|
||||
|
||||
/// The resume data generated if the request was cancelled.
|
||||
public let resumeData: Data?
|
||||
|
||||
/// The result of response serialization.
|
||||
public let result: Result<Value>
|
||||
|
||||
/// The timeline of the complete lifecycle of the request.
|
||||
public let timeline: Timeline
|
||||
|
||||
/// Returns the associated value of the result if it is a success, `nil` otherwise.
|
||||
public var value: Value? { return result.value }
|
||||
|
||||
/// Returns the associated error value if the result if it is a failure, `nil` otherwise.
|
||||
public var error: Error? { return result.error }
|
||||
|
||||
var _metrics: AnyObject?
|
||||
|
||||
/// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization.
|
||||
///
|
||||
/// - parameter request: The URL request sent to the server.
|
||||
/// - parameter response: The server's response to the URL request.
|
||||
/// - parameter temporaryURL: The temporary destination URL of the data returned from the server.
|
||||
/// - parameter destinationURL: The final destination URL of the data returned from the server if it was moved.
|
||||
/// - parameter resumeData: The resume data generated if the request was cancelled.
|
||||
/// - parameter result: The result of response serialization.
|
||||
/// - parameter timeline: The timeline of the complete lifecycle of the `Request`. Defaults to `Timeline()`.
|
||||
///
|
||||
/// - returns: The new `DownloadResponse` instance.
|
||||
public init(
|
||||
request: URLRequest?,
|
||||
response: HTTPURLResponse?,
|
||||
temporaryURL: URL?,
|
||||
destinationURL: URL?,
|
||||
resumeData: Data?,
|
||||
result: Result<Value>,
|
||||
timeline: Timeline = Timeline())
|
||||
{
|
||||
self.request = request
|
||||
self.response = response
|
||||
self.temporaryURL = temporaryURL
|
||||
self.destinationURL = destinationURL
|
||||
self.resumeData = resumeData
|
||||
self.result = result
|
||||
self.timeline = timeline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
/// The textual representation used when written to an output stream, which includes whether the result was a
|
||||
/// success or failure.
|
||||
public var description: String {
|
||||
return result.debugDescription
|
||||
}
|
||||
|
||||
/// The debug textual representation used when written to an output stream, which includes the URL request, the URL
|
||||
/// response, the temporary and destination URLs, the resume data, the response serialization result and the
|
||||
/// timeline.
|
||||
public var debugDescription: String {
|
||||
var output: [String] = []
|
||||
|
||||
output.append(request != nil ? "[Request]: \(request!.httpMethod ?? "GET") \(request!)" : "[Request]: nil")
|
||||
output.append(response != nil ? "[Response]: \(response!)" : "[Response]: nil")
|
||||
output.append("[TemporaryURL]: \(temporaryURL?.path ?? "nil")")
|
||||
output.append("[DestinationURL]: \(destinationURL?.path ?? "nil")")
|
||||
output.append("[ResumeData]: \(resumeData?.count ?? 0) bytes")
|
||||
output.append("[Result]: \(result.debugDescription)")
|
||||
output.append("[Timeline]: \(timeline.debugDescription)")
|
||||
|
||||
return output.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension DownloadResponse {
|
||||
/// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped
|
||||
/// result value as a parameter.
|
||||
///
|
||||
/// Use the `map` method with a closure that does not throw. For example:
|
||||
///
|
||||
/// let possibleData: DownloadResponse<Data> = ...
|
||||
/// let possibleInt = possibleData.map { $0.count }
|
||||
///
|
||||
/// - parameter transform: A closure that takes the success value of the instance's result.
|
||||
///
|
||||
/// - returns: A `DownloadResponse` whose result wraps the value returned by the given closure. If this instance's
|
||||
/// result is a failure, returns a response wrapping the same failure.
|
||||
public func map<T>(_ transform: (Value) -> T) -> DownloadResponse<T> {
|
||||
var response = DownloadResponse<T>(
|
||||
request: request,
|
||||
response: self.response,
|
||||
temporaryURL: temporaryURL,
|
||||
destinationURL: destinationURL,
|
||||
resumeData: resumeData,
|
||||
result: result.map(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped
|
||||
/// result value as a parameter.
|
||||
///
|
||||
/// Use the `flatMap` method with a closure that may throw an error. For example:
|
||||
///
|
||||
/// let possibleData: DownloadResponse<Data> = ...
|
||||
/// let possibleObject = possibleData.flatMap {
|
||||
/// try JSONSerialization.jsonObject(with: $0)
|
||||
/// }
|
||||
///
|
||||
/// - parameter transform: A closure that takes the success value of the instance's result.
|
||||
///
|
||||
/// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this
|
||||
/// instance's result is a failure, returns the same failure.
|
||||
public func flatMap<T>(_ transform: (Value) throws -> T) -> DownloadResponse<T> {
|
||||
var response = DownloadResponse<T>(
|
||||
request: request,
|
||||
response: self.response,
|
||||
temporaryURL: temporaryURL,
|
||||
destinationURL: destinationURL,
|
||||
resumeData: resumeData,
|
||||
result: result.flatMap(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `mapError` function with a closure that does not throw. For example:
|
||||
///
|
||||
/// let possibleData: DownloadResponse<Data> = ...
|
||||
/// let withMyError = possibleData.mapError { MyError.error($0) }
|
||||
///
|
||||
/// - Parameter transform: A closure that takes the error of the instance.
|
||||
/// - Returns: A `DownloadResponse` instance containing the result of the transform.
|
||||
public func mapError<E: Error>(_ transform: (Error) -> E) -> DownloadResponse {
|
||||
var response = DownloadResponse(
|
||||
request: request,
|
||||
response: self.response,
|
||||
temporaryURL: temporaryURL,
|
||||
destinationURL: destinationURL,
|
||||
resumeData: resumeData,
|
||||
result: result.mapError(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `flatMapError` function with a closure that may throw an error. For example:
|
||||
///
|
||||
/// let possibleData: DownloadResponse<Data> = ...
|
||||
/// let possibleObject = possibleData.flatMapError {
|
||||
/// try someFailableFunction(taking: $0)
|
||||
/// }
|
||||
///
|
||||
/// - Parameter transform: A throwing closure that takes the error of the instance.
|
||||
///
|
||||
/// - Returns: A `DownloadResponse` instance containing the result of the transform.
|
||||
public func flatMapError<E: Error>(_ transform: (Error) throws -> E) -> DownloadResponse {
|
||||
var response = DownloadResponse(
|
||||
request: request,
|
||||
response: self.response,
|
||||
temporaryURL: temporaryURL,
|
||||
destinationURL: destinationURL,
|
||||
resumeData: resumeData,
|
||||
result: result.flatMapError(transform),
|
||||
timeline: timeline
|
||||
)
|
||||
|
||||
response._metrics = _metrics
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
protocol Response {
|
||||
/// The task metrics containing the request / response statistics.
|
||||
var _metrics: AnyObject? { get set }
|
||||
mutating func add(_ metrics: AnyObject?)
|
||||
}
|
||||
|
||||
extension Response {
|
||||
mutating func add(_ metrics: AnyObject?) {
|
||||
#if !os(watchOS)
|
||||
guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) else { return }
|
||||
guard let metrics = metrics as? URLSessionTaskMetrics else { return }
|
||||
|
||||
_metrics = metrics
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
extension DefaultDataResponse: Response {
|
||||
#if !os(watchOS)
|
||||
/// The task metrics containing the request / response statistics.
|
||||
public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
extension DataResponse: Response {
|
||||
#if !os(watchOS)
|
||||
/// The task metrics containing the request / response statistics.
|
||||
public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
extension DefaultDownloadResponse: Response {
|
||||
#if !os(watchOS)
|
||||
/// The task metrics containing the request / response statistics.
|
||||
public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
extension DownloadResponse: Response {
|
||||
#if !os(watchOS)
|
||||
/// The task metrics containing the request / response statistics.
|
||||
public var metrics: URLSessionTaskMetrics? { return _metrics as? URLSessionTaskMetrics }
|
||||
#endif
|
||||
}
|
||||
715
Pods/Alamofire/Source/ResponseSerialization.swift
generated
Normal file
715
Pods/Alamofire/Source/ResponseSerialization.swift
generated
Normal file
@ -0,0 +1,715 @@
|
||||
//
|
||||
// ResponseSerialization.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The type in which all data response serializers must conform to in order to serialize a response.
|
||||
public protocol DataResponseSerializerProtocol {
|
||||
/// The type of serialized object to be created by this `DataResponseSerializerType`.
|
||||
associatedtype SerializedObject
|
||||
|
||||
/// A closure used by response handlers that takes a request, response, data and error and returns a result.
|
||||
var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result<SerializedObject> { get }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// A generic `DataResponseSerializerType` used to serialize a request, response, and data into a serialized object.
|
||||
public struct DataResponseSerializer<Value>: DataResponseSerializerProtocol {
|
||||
/// The type of serialized object to be created by this `DataResponseSerializer`.
|
||||
public typealias SerializedObject = Value
|
||||
|
||||
/// A closure used by response handlers that takes a request, response, data and error and returns a result.
|
||||
public var serializeResponse: (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result<Value>
|
||||
|
||||
/// Initializes the `ResponseSerializer` instance with the given serialize response closure.
|
||||
///
|
||||
/// - parameter serializeResponse: The closure used to serialize the response.
|
||||
///
|
||||
/// - returns: The new generic response serializer instance.
|
||||
public init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Result<Value>) {
|
||||
self.serializeResponse = serializeResponse
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// The type in which all download response serializers must conform to in order to serialize a response.
|
||||
public protocol DownloadResponseSerializerProtocol {
|
||||
/// The type of serialized object to be created by this `DownloadResponseSerializerType`.
|
||||
associatedtype SerializedObject
|
||||
|
||||
/// A closure used by response handlers that takes a request, response, url and error and returns a result.
|
||||
var serializeResponse: (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result<SerializedObject> { get }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// A generic `DownloadResponseSerializerType` used to serialize a request, response, and data into a serialized object.
|
||||
public struct DownloadResponseSerializer<Value>: DownloadResponseSerializerProtocol {
|
||||
/// The type of serialized object to be created by this `DownloadResponseSerializer`.
|
||||
public typealias SerializedObject = Value
|
||||
|
||||
/// A closure used by response handlers that takes a request, response, url and error and returns a result.
|
||||
public var serializeResponse: (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result<Value>
|
||||
|
||||
/// Initializes the `ResponseSerializer` instance with the given serialize response closure.
|
||||
///
|
||||
/// - parameter serializeResponse: The closure used to serialize the response.
|
||||
///
|
||||
/// - returns: The new generic response serializer instance.
|
||||
public init(serializeResponse: @escaping (URLRequest?, HTTPURLResponse?, URL?, Error?) -> Result<Value>) {
|
||||
self.serializeResponse = serializeResponse
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline
|
||||
|
||||
extension Request {
|
||||
var timeline: Timeline {
|
||||
let requestStartTime = self.startTime ?? CFAbsoluteTimeGetCurrent()
|
||||
let requestCompletedTime = self.endTime ?? CFAbsoluteTimeGetCurrent()
|
||||
let initialResponseTime = self.delegate.initialResponseTime ?? requestCompletedTime
|
||||
|
||||
return Timeline(
|
||||
requestStartTime: requestStartTime,
|
||||
initialResponseTime: initialResponseTime,
|
||||
requestCompletedTime: requestCompletedTime,
|
||||
serializationCompletedTime: CFAbsoluteTimeGetCurrent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Default
|
||||
|
||||
extension DataRequest {
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter queue: The queue on which the completion handler is dispatched.
|
||||
/// - parameter completionHandler: The code to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DefaultDataResponse) -> Void) -> Self {
|
||||
delegate.queue.addOperation {
|
||||
(queue ?? DispatchQueue.main).async {
|
||||
var dataResponse = DefaultDataResponse(
|
||||
request: self.request,
|
||||
response: self.response,
|
||||
data: self.delegate.data,
|
||||
error: self.delegate.error,
|
||||
timeline: self.timeline
|
||||
)
|
||||
|
||||
dataResponse.add(self.delegate.metrics)
|
||||
|
||||
completionHandler(dataResponse)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter queue: The queue on which the completion handler is dispatched.
|
||||
/// - parameter responseSerializer: The response serializer responsible for serializing the request, response,
|
||||
/// and data.
|
||||
/// - parameter completionHandler: The code to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func response<T: DataResponseSerializerProtocol>(
|
||||
queue: DispatchQueue? = nil,
|
||||
responseSerializer: T,
|
||||
completionHandler: @escaping (DataResponse<T.SerializedObject>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
delegate.queue.addOperation {
|
||||
let result = responseSerializer.serializeResponse(
|
||||
self.request,
|
||||
self.response,
|
||||
self.delegate.data,
|
||||
self.delegate.error
|
||||
)
|
||||
|
||||
var dataResponse = DataResponse<T.SerializedObject>(
|
||||
request: self.request,
|
||||
response: self.response,
|
||||
data: self.delegate.data,
|
||||
result: result,
|
||||
timeline: self.timeline
|
||||
)
|
||||
|
||||
dataResponse.add(self.delegate.metrics)
|
||||
|
||||
(queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadRequest {
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter queue: The queue on which the completion handler is dispatched.
|
||||
/// - parameter completionHandler: The code to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func response(
|
||||
queue: DispatchQueue? = nil,
|
||||
completionHandler: @escaping (DefaultDownloadResponse) -> Void)
|
||||
-> Self
|
||||
{
|
||||
delegate.queue.addOperation {
|
||||
(queue ?? DispatchQueue.main).async {
|
||||
var downloadResponse = DefaultDownloadResponse(
|
||||
request: self.request,
|
||||
response: self.response,
|
||||
temporaryURL: self.downloadDelegate.temporaryURL,
|
||||
destinationURL: self.downloadDelegate.destinationURL,
|
||||
resumeData: self.downloadDelegate.resumeData,
|
||||
error: self.downloadDelegate.error,
|
||||
timeline: self.timeline
|
||||
)
|
||||
|
||||
downloadResponse.add(self.delegate.metrics)
|
||||
|
||||
completionHandler(downloadResponse)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter queue: The queue on which the completion handler is dispatched.
|
||||
/// - parameter responseSerializer: The response serializer responsible for serializing the request, response,
|
||||
/// and data contained in the destination url.
|
||||
/// - parameter completionHandler: The code to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func response<T: DownloadResponseSerializerProtocol>(
|
||||
queue: DispatchQueue? = nil,
|
||||
responseSerializer: T,
|
||||
completionHandler: @escaping (DownloadResponse<T.SerializedObject>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
delegate.queue.addOperation {
|
||||
let result = responseSerializer.serializeResponse(
|
||||
self.request,
|
||||
self.response,
|
||||
self.downloadDelegate.fileURL,
|
||||
self.downloadDelegate.error
|
||||
)
|
||||
|
||||
var downloadResponse = DownloadResponse<T.SerializedObject>(
|
||||
request: self.request,
|
||||
response: self.response,
|
||||
temporaryURL: self.downloadDelegate.temporaryURL,
|
||||
destinationURL: self.downloadDelegate.destinationURL,
|
||||
resumeData: self.downloadDelegate.resumeData,
|
||||
result: result,
|
||||
timeline: self.timeline
|
||||
)
|
||||
|
||||
downloadResponse.add(self.delegate.metrics)
|
||||
|
||||
(queue ?? DispatchQueue.main).async { completionHandler(downloadResponse) }
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
extension Request {
|
||||
/// Returns a result data type that contains the response data as-is.
|
||||
///
|
||||
/// - parameter response: The response from the server.
|
||||
/// - parameter data: The data returned from the server.
|
||||
/// - parameter error: The error already encountered if it exists.
|
||||
///
|
||||
/// - returns: The result data type.
|
||||
public static func serializeResponseData(response: HTTPURLResponse?, data: Data?, error: Error?) -> Result<Data> {
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(Data()) }
|
||||
|
||||
guard let validData = data else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
|
||||
}
|
||||
|
||||
return .success(validData)
|
||||
}
|
||||
}
|
||||
|
||||
extension DataRequest {
|
||||
/// Creates a response serializer that returns the associated data as-is.
|
||||
///
|
||||
/// - returns: A data response serializer.
|
||||
public static func dataResponseSerializer() -> DataResponseSerializer<Data> {
|
||||
return DataResponseSerializer { _, response, data, error in
|
||||
return Request.serializeResponseData(response: response, data: data, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter completionHandler: The code to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseData(
|
||||
queue: DispatchQueue? = nil,
|
||||
completionHandler: @escaping (DataResponse<Data>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DataRequest.dataResponseSerializer(),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadRequest {
|
||||
/// Creates a response serializer that returns the associated data as-is.
|
||||
///
|
||||
/// - returns: A data response serializer.
|
||||
public static func dataResponseSerializer() -> DownloadResponseSerializer<Data> {
|
||||
return DownloadResponseSerializer { _, response, fileURL, error in
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
guard let fileURL = fileURL else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return Request.serializeResponseData(response: response, data: data, error: error)
|
||||
} catch {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter completionHandler: The code to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseData(
|
||||
queue: DispatchQueue? = nil,
|
||||
completionHandler: @escaping (DownloadResponse<Data>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DownloadRequest.dataResponseSerializer(),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String
|
||||
|
||||
extension Request {
|
||||
/// Returns a result string type initialized from the response data with the specified string encoding.
|
||||
///
|
||||
/// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server
|
||||
/// response, falling back to the default HTTP default character set, ISO-8859-1.
|
||||
/// - parameter response: The response from the server.
|
||||
/// - parameter data: The data returned from the server.
|
||||
/// - parameter error: The error already encountered if it exists.
|
||||
///
|
||||
/// - returns: The result data type.
|
||||
public static func serializeResponseString(
|
||||
encoding: String.Encoding?,
|
||||
response: HTTPURLResponse?,
|
||||
data: Data?,
|
||||
error: Error?)
|
||||
-> Result<String>
|
||||
{
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success("") }
|
||||
|
||||
guard let validData = data else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
|
||||
}
|
||||
|
||||
var convertedEncoding = encoding
|
||||
|
||||
if let encodingName = response?.textEncodingName as CFString?, convertedEncoding == nil {
|
||||
convertedEncoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(
|
||||
CFStringConvertIANACharSetNameToEncoding(encodingName))
|
||||
)
|
||||
}
|
||||
|
||||
let actualEncoding = convertedEncoding ?? .isoLatin1
|
||||
|
||||
if let string = String(data: validData, encoding: actualEncoding) {
|
||||
return .success(string)
|
||||
} else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataRequest {
|
||||
/// Creates a response serializer that returns a result string type initialized from the response data with
|
||||
/// the specified string encoding.
|
||||
///
|
||||
/// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server
|
||||
/// response, falling back to the default HTTP default character set, ISO-8859-1.
|
||||
///
|
||||
/// - returns: A string response serializer.
|
||||
public static func stringResponseSerializer(encoding: String.Encoding? = nil) -> DataResponseSerializer<String> {
|
||||
return DataResponseSerializer { _, response, data, error in
|
||||
return Request.serializeResponseString(encoding: encoding, response: response, data: data, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the
|
||||
/// server response, falling back to the default HTTP default character set,
|
||||
/// ISO-8859-1.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseString(
|
||||
queue: DispatchQueue? = nil,
|
||||
encoding: String.Encoding? = nil,
|
||||
completionHandler: @escaping (DataResponse<String>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DataRequest.stringResponseSerializer(encoding: encoding),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadRequest {
|
||||
/// Creates a response serializer that returns a result string type initialized from the response data with
|
||||
/// the specified string encoding.
|
||||
///
|
||||
/// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the server
|
||||
/// response, falling back to the default HTTP default character set, ISO-8859-1.
|
||||
///
|
||||
/// - returns: A string response serializer.
|
||||
public static func stringResponseSerializer(encoding: String.Encoding? = nil) -> DownloadResponseSerializer<String> {
|
||||
return DownloadResponseSerializer { _, response, fileURL, error in
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
guard let fileURL = fileURL else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return Request.serializeResponseString(encoding: encoding, response: response, data: data, error: error)
|
||||
} catch {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter encoding: The string encoding. If `nil`, the string encoding will be determined from the
|
||||
/// server response, falling back to the default HTTP default character set,
|
||||
/// ISO-8859-1.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseString(
|
||||
queue: DispatchQueue? = nil,
|
||||
encoding: String.Encoding? = nil,
|
||||
completionHandler: @escaping (DownloadResponse<String>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DownloadRequest.stringResponseSerializer(encoding: encoding),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JSON
|
||||
|
||||
extension Request {
|
||||
/// Returns a JSON object contained in a result type constructed from the response data using `JSONSerialization`
|
||||
/// with the specified reading options.
|
||||
///
|
||||
/// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
|
||||
/// - parameter response: The response from the server.
|
||||
/// - parameter data: The data returned from the server.
|
||||
/// - parameter error: The error already encountered if it exists.
|
||||
///
|
||||
/// - returns: The result data type.
|
||||
public static func serializeResponseJSON(
|
||||
options: JSONSerialization.ReadingOptions,
|
||||
response: HTTPURLResponse?,
|
||||
data: Data?,
|
||||
error: Error?)
|
||||
-> Result<Any>
|
||||
{
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) }
|
||||
|
||||
guard let validData = data, validData.count > 0 else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
|
||||
}
|
||||
|
||||
do {
|
||||
let json = try JSONSerialization.jsonObject(with: validData, options: options)
|
||||
return .success(json)
|
||||
} catch {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataRequest {
|
||||
/// Creates a response serializer that returns a JSON object result type constructed from the response data using
|
||||
/// `JSONSerialization` with the specified reading options.
|
||||
///
|
||||
/// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
|
||||
///
|
||||
/// - returns: A JSON object response serializer.
|
||||
public static func jsonResponseSerializer(
|
||||
options: JSONSerialization.ReadingOptions = .allowFragments)
|
||||
-> DataResponseSerializer<Any>
|
||||
{
|
||||
return DataResponseSerializer { _, response, data, error in
|
||||
return Request.serializeResponseJSON(options: options, response: response, data: data, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseJSON(
|
||||
queue: DispatchQueue? = nil,
|
||||
options: JSONSerialization.ReadingOptions = .allowFragments,
|
||||
completionHandler: @escaping (DataResponse<Any>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DataRequest.jsonResponseSerializer(options: options),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadRequest {
|
||||
/// Creates a response serializer that returns a JSON object result type constructed from the response data using
|
||||
/// `JSONSerialization` with the specified reading options.
|
||||
///
|
||||
/// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
|
||||
///
|
||||
/// - returns: A JSON object response serializer.
|
||||
public static func jsonResponseSerializer(
|
||||
options: JSONSerialization.ReadingOptions = .allowFragments)
|
||||
-> DownloadResponseSerializer<Any>
|
||||
{
|
||||
return DownloadResponseSerializer { _, response, fileURL, error in
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
guard let fileURL = fileURL else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return Request.serializeResponseJSON(options: options, response: response, data: data, error: error)
|
||||
} catch {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter options: The JSON serialization reading options. Defaults to `.allowFragments`.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseJSON(
|
||||
queue: DispatchQueue? = nil,
|
||||
options: JSONSerialization.ReadingOptions = .allowFragments,
|
||||
completionHandler: @escaping (DownloadResponse<Any>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DownloadRequest.jsonResponseSerializer(options: options),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property List
|
||||
|
||||
extension Request {
|
||||
/// Returns a plist object contained in a result type constructed from the response data using
|
||||
/// `PropertyListSerialization` with the specified reading options.
|
||||
///
|
||||
/// - parameter options: The property list reading options. Defaults to `[]`.
|
||||
/// - parameter response: The response from the server.
|
||||
/// - parameter data: The data returned from the server.
|
||||
/// - parameter error: The error already encountered if it exists.
|
||||
///
|
||||
/// - returns: The result data type.
|
||||
public static func serializeResponsePropertyList(
|
||||
options: PropertyListSerialization.ReadOptions,
|
||||
response: HTTPURLResponse?,
|
||||
data: Data?,
|
||||
error: Error?)
|
||||
-> Result<Any>
|
||||
{
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()) }
|
||||
|
||||
guard let validData = data, validData.count > 0 else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
|
||||
}
|
||||
|
||||
do {
|
||||
let plist = try PropertyListSerialization.propertyList(from: validData, options: options, format: nil)
|
||||
return .success(plist)
|
||||
} catch {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .propertyListSerializationFailed(error: error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataRequest {
|
||||
/// Creates a response serializer that returns an object constructed from the response data using
|
||||
/// `PropertyListSerialization` with the specified reading options.
|
||||
///
|
||||
/// - parameter options: The property list reading options. Defaults to `[]`.
|
||||
///
|
||||
/// - returns: A property list object response serializer.
|
||||
public static func propertyListResponseSerializer(
|
||||
options: PropertyListSerialization.ReadOptions = [])
|
||||
-> DataResponseSerializer<Any>
|
||||
{
|
||||
return DataResponseSerializer { _, response, data, error in
|
||||
return Request.serializeResponsePropertyList(options: options, response: response, data: data, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter options: The property list reading options. Defaults to `[]`.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responsePropertyList(
|
||||
queue: DispatchQueue? = nil,
|
||||
options: PropertyListSerialization.ReadOptions = [],
|
||||
completionHandler: @escaping (DataResponse<Any>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DataRequest.propertyListResponseSerializer(options: options),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadRequest {
|
||||
/// Creates a response serializer that returns an object constructed from the response data using
|
||||
/// `PropertyListSerialization` with the specified reading options.
|
||||
///
|
||||
/// - parameter options: The property list reading options. Defaults to `[]`.
|
||||
///
|
||||
/// - returns: A property list object response serializer.
|
||||
public static func propertyListResponseSerializer(
|
||||
options: PropertyListSerialization.ReadOptions = [])
|
||||
-> DownloadResponseSerializer<Any>
|
||||
{
|
||||
return DownloadResponseSerializer { _, response, fileURL, error in
|
||||
guard error == nil else { return .failure(error!) }
|
||||
|
||||
guard let fileURL = fileURL else {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileNil))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return Request.serializeResponsePropertyList(options: options, response: response, data: data, error: error)
|
||||
} catch {
|
||||
return .failure(AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter options: The property list reading options. Defaults to `[]`.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responsePropertyList(
|
||||
queue: DispatchQueue? = nil,
|
||||
options: PropertyListSerialization.ReadOptions = [],
|
||||
completionHandler: @escaping (DownloadResponse<Any>) -> Void)
|
||||
-> Self
|
||||
{
|
||||
return response(
|
||||
queue: queue,
|
||||
responseSerializer: DownloadRequest.propertyListResponseSerializer(options: options),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of HTTP response status code that do not contain response data.
|
||||
private let emptyDataStatusCodes: Set<Int> = [204, 205]
|
||||
300
Pods/Alamofire/Source/Result.swift
generated
Normal file
300
Pods/Alamofire/Source/Result.swift
generated
Normal file
@ -0,0 +1,300 @@
|
||||
//
|
||||
// Result.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Used to represent whether a request was successful or encountered an error.
|
||||
///
|
||||
/// - success: The request and all post processing operations were successful resulting in the serialization of the
|
||||
/// provided associated value.
|
||||
///
|
||||
/// - failure: The request encountered an error resulting in a failure. The associated values are the original data
|
||||
/// provided by the server as well as the error that caused the failure.
|
||||
public enum Result<Value> {
|
||||
case success(Value)
|
||||
case failure(Error)
|
||||
|
||||
/// Returns `true` if the result is a success, `false` otherwise.
|
||||
public var isSuccess: Bool {
|
||||
switch self {
|
||||
case .success:
|
||||
return true
|
||||
case .failure:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the result is a failure, `false` otherwise.
|
||||
public var isFailure: Bool {
|
||||
return !isSuccess
|
||||
}
|
||||
|
||||
/// Returns the associated value if the result is a success, `nil` otherwise.
|
||||
public var value: Value? {
|
||||
switch self {
|
||||
case .success(let value):
|
||||
return value
|
||||
case .failure:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the associated error value if the result is a failure, `nil` otherwise.
|
||||
public var error: Error? {
|
||||
switch self {
|
||||
case .success:
|
||||
return nil
|
||||
case .failure(let error):
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Result: CustomStringConvertible {
|
||||
/// The textual representation used when written to an output stream, which includes whether the result was a
|
||||
/// success or failure.
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .success:
|
||||
return "SUCCESS"
|
||||
case .failure:
|
||||
return "FAILURE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomDebugStringConvertible
|
||||
|
||||
extension Result: CustomDebugStringConvertible {
|
||||
/// The debug textual representation used when written to an output stream, which includes whether the result was a
|
||||
/// success or failure in addition to the value or error.
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .success(let value):
|
||||
return "SUCCESS: \(value)"
|
||||
case .failure(let error):
|
||||
return "FAILURE: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Functional APIs
|
||||
|
||||
extension Result {
|
||||
/// Creates a `Result` instance from the result of a closure.
|
||||
///
|
||||
/// A failure result is created when the closure throws, and a success result is created when the closure
|
||||
/// succeeds without throwing an error.
|
||||
///
|
||||
/// func someString() throws -> String { ... }
|
||||
///
|
||||
/// let result = Result(value: {
|
||||
/// return try someString()
|
||||
/// })
|
||||
///
|
||||
/// // The type of result is Result<String>
|
||||
///
|
||||
/// The trailing closure syntax is also supported:
|
||||
///
|
||||
/// let result = Result { try someString() }
|
||||
///
|
||||
/// - parameter value: The closure to execute and create the result for.
|
||||
public init(value: () throws -> Value) {
|
||||
do {
|
||||
self = try .success(value())
|
||||
} catch {
|
||||
self = .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the success value, or throws the failure error.
|
||||
///
|
||||
/// let possibleString: Result<String> = .success("success")
|
||||
/// try print(possibleString.unwrap())
|
||||
/// // Prints "success"
|
||||
///
|
||||
/// let noString: Result<String> = .failure(error)
|
||||
/// try print(noString.unwrap())
|
||||
/// // Throws error
|
||||
public func unwrap() throws -> Value {
|
||||
switch self {
|
||||
case .success(let value):
|
||||
return value
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter.
|
||||
///
|
||||
/// Use the `map` method with a closure that does not throw. For example:
|
||||
///
|
||||
/// let possibleData: Result<Data> = .success(Data())
|
||||
/// let possibleInt = possibleData.map { $0.count }
|
||||
/// try print(possibleInt.unwrap())
|
||||
/// // Prints "0"
|
||||
///
|
||||
/// let noData: Result<Data> = .failure(error)
|
||||
/// let noInt = noData.map { $0.count }
|
||||
/// try print(noInt.unwrap())
|
||||
/// // Throws error
|
||||
///
|
||||
/// - parameter transform: A closure that takes the success value of the `Result` instance.
|
||||
///
|
||||
/// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the
|
||||
/// same failure.
|
||||
public func map<T>(_ transform: (Value) -> T) -> Result<T> {
|
||||
switch self {
|
||||
case .success(let value):
|
||||
return .success(transform(value))
|
||||
case .failure(let error):
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter.
|
||||
///
|
||||
/// Use the `flatMap` method with a closure that may throw an error. For example:
|
||||
///
|
||||
/// let possibleData: Result<Data> = .success(Data(...))
|
||||
/// let possibleObject = possibleData.flatMap {
|
||||
/// try JSONSerialization.jsonObject(with: $0)
|
||||
/// }
|
||||
///
|
||||
/// - parameter transform: A closure that takes the success value of the instance.
|
||||
///
|
||||
/// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the
|
||||
/// same failure.
|
||||
public func flatMap<T>(_ transform: (Value) throws -> T) -> Result<T> {
|
||||
switch self {
|
||||
case .success(let value):
|
||||
do {
|
||||
return try .success(transform(value))
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
case .failure(let error):
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `mapError` function with a closure that does not throw. For example:
|
||||
///
|
||||
/// let possibleData: Result<Data> = .failure(someError)
|
||||
/// let withMyError: Result<Data> = possibleData.mapError { MyError.error($0) }
|
||||
///
|
||||
/// - Parameter transform: A closure that takes the error of the instance.
|
||||
/// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
|
||||
/// the same instance.
|
||||
public func mapError<T: Error>(_ transform: (Error) -> T) -> Result {
|
||||
switch self {
|
||||
case .failure(let error):
|
||||
return .failure(transform(error))
|
||||
case .success:
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `flatMapError` function with a closure that may throw an error. For example:
|
||||
///
|
||||
/// let possibleData: Result<Data> = .success(Data(...))
|
||||
/// let possibleObject = possibleData.flatMapError {
|
||||
/// try someFailableFunction(taking: $0)
|
||||
/// }
|
||||
///
|
||||
/// - Parameter transform: A throwing closure that takes the error of the instance.
|
||||
///
|
||||
/// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
|
||||
/// the same instance.
|
||||
public func flatMapError<T: Error>(_ transform: (Error) throws -> T) -> Result {
|
||||
switch self {
|
||||
case .failure(let error):
|
||||
do {
|
||||
return try .failure(transform(error))
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
case .success:
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter.
|
||||
///
|
||||
/// Use the `withValue` function to evaluate the passed closure without modifying the `Result` instance.
|
||||
///
|
||||
/// - Parameter closure: A closure that takes the success value of this instance.
|
||||
/// - Returns: This `Result` instance, unmodified.
|
||||
@discardableResult
|
||||
public func withValue(_ closure: (Value) -> Void) -> Result {
|
||||
if case let .success(value) = self { closure(value) }
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
|
||||
///
|
||||
/// Use the `withError` function to evaluate the passed closure without modifying the `Result` instance.
|
||||
///
|
||||
/// - Parameter closure: A closure that takes the success value of this instance.
|
||||
/// - Returns: This `Result` instance, unmodified.
|
||||
@discardableResult
|
||||
public func withError(_ closure: (Error) -> Void) -> Result {
|
||||
if case let .failure(error) = self { closure(error) }
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a success.
|
||||
///
|
||||
/// Use the `ifSuccess` function to evaluate the passed closure without modifying the `Result` instance.
|
||||
///
|
||||
/// - Parameter closure: A `Void` closure.
|
||||
/// - Returns: This `Result` instance, unmodified.
|
||||
@discardableResult
|
||||
public func ifSuccess(_ closure: () -> Void) -> Result {
|
||||
if isSuccess { closure() }
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Evaluates the specified closure when the `Result` is a failure.
|
||||
///
|
||||
/// Use the `ifFailure` function to evaluate the passed closure without modifying the `Result` instance.
|
||||
///
|
||||
/// - Parameter closure: A `Void` closure.
|
||||
/// - Returns: This `Result` instance, unmodified.
|
||||
@discardableResult
|
||||
public func ifFailure(_ closure: () -> Void) -> Result {
|
||||
if isFailure { closure() }
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
307
Pods/Alamofire/Source/ServerTrustPolicy.swift
generated
Normal file
307
Pods/Alamofire/Source/ServerTrustPolicy.swift
generated
Normal file
@ -0,0 +1,307 @@
|
||||
//
|
||||
// ServerTrustPolicy.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host.
|
||||
open class ServerTrustPolicyManager {
|
||||
/// The dictionary of policies mapped to a particular host.
|
||||
public let policies: [String: ServerTrustPolicy]
|
||||
|
||||
/// Initializes the `ServerTrustPolicyManager` instance with the given policies.
|
||||
///
|
||||
/// Since different servers and web services can have different leaf certificates, intermediate and even root
|
||||
/// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This
|
||||
/// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key
|
||||
/// pinning for host3 and disabling evaluation for host4.
|
||||
///
|
||||
/// - parameter policies: A dictionary of all policies mapped to a particular host.
|
||||
///
|
||||
/// - returns: The new `ServerTrustPolicyManager` instance.
|
||||
public init(policies: [String: ServerTrustPolicy]) {
|
||||
self.policies = policies
|
||||
}
|
||||
|
||||
/// Returns the `ServerTrustPolicy` for the given host if applicable.
|
||||
///
|
||||
/// By default, this method will return the policy that perfectly matches the given host. Subclasses could override
|
||||
/// this method and implement more complex mapping implementations such as wildcards.
|
||||
///
|
||||
/// - parameter host: The host to use when searching for a matching policy.
|
||||
///
|
||||
/// - returns: The server trust policy for the given host if found.
|
||||
open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
|
||||
return policies[host]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension URLSession {
|
||||
private struct AssociatedKeys {
|
||||
static var managerKey = "URLSession.ServerTrustPolicyManager"
|
||||
}
|
||||
|
||||
var serverTrustPolicyManager: ServerTrustPolicyManager? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager
|
||||
}
|
||||
set (manager) {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ServerTrustPolicy
|
||||
|
||||
/// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when
|
||||
/// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust
|
||||
/// with a given set of criteria to determine whether the server trust is valid and the connection should be made.
|
||||
///
|
||||
/// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other
|
||||
/// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged
|
||||
/// to route all communication over an HTTPS connection with pinning enabled.
|
||||
///
|
||||
/// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to
|
||||
/// validate the host provided by the challenge. Applications are encouraged to always
|
||||
/// validate the host in production environments to guarantee the validity of the server's
|
||||
/// certificate chain.
|
||||
///
|
||||
/// - performRevokedEvaluation: Uses the default and revoked server trust evaluations allowing you to control whether to
|
||||
/// validate the host provided by the challenge as well as specify the revocation flags for
|
||||
/// testing for revoked certificates. Apple platforms did not start testing for revoked
|
||||
/// certificates automatically until iOS 10.1, macOS 10.12 and tvOS 10.1 which is
|
||||
/// demonstrated in our TLS tests. Applications are encouraged to always validate the host
|
||||
/// in production environments to guarantee the validity of the server's certificate chain.
|
||||
///
|
||||
/// - pinCertificates: Uses the pinned certificates to validate the server trust. The server trust is
|
||||
/// considered valid if one of the pinned certificates match one of the server certificates.
|
||||
/// By validating both the certificate chain and host, certificate pinning provides a very
|
||||
/// secure form of server trust validation mitigating most, if not all, MITM attacks.
|
||||
/// Applications are encouraged to always validate the host and require a valid certificate
|
||||
/// chain in production environments.
|
||||
///
|
||||
/// - pinPublicKeys: Uses the pinned public keys to validate the server trust. The server trust is considered
|
||||
/// valid if one of the pinned public keys match one of the server certificate public keys.
|
||||
/// By validating both the certificate chain and host, public key pinning provides a very
|
||||
/// secure form of server trust validation mitigating most, if not all, MITM attacks.
|
||||
/// Applications are encouraged to always validate the host and require a valid certificate
|
||||
/// chain in production environments.
|
||||
///
|
||||
/// - disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid.
|
||||
///
|
||||
/// - customEvaluation: Uses the associated closure to evaluate the validity of the server trust.
|
||||
public enum ServerTrustPolicy {
|
||||
case performDefaultEvaluation(validateHost: Bool)
|
||||
case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags)
|
||||
case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)
|
||||
case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool)
|
||||
case disableEvaluation
|
||||
case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool)
|
||||
|
||||
// MARK: - Bundle Location
|
||||
|
||||
/// Returns all certificates within the given bundle with a `.cer` file extension.
|
||||
///
|
||||
/// - parameter bundle: The bundle to search for all `.cer` files.
|
||||
///
|
||||
/// - returns: All certificates within the given bundle.
|
||||
public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] {
|
||||
var certificates: [SecCertificate] = []
|
||||
|
||||
let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in
|
||||
bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil)
|
||||
}.joined())
|
||||
|
||||
for path in paths {
|
||||
if
|
||||
let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
|
||||
let certificate = SecCertificateCreateWithData(nil, certificateData)
|
||||
{
|
||||
certificates.append(certificate)
|
||||
}
|
||||
}
|
||||
|
||||
return certificates
|
||||
}
|
||||
|
||||
/// Returns all public keys within the given bundle with a `.cer` file extension.
|
||||
///
|
||||
/// - parameter bundle: The bundle to search for all `*.cer` files.
|
||||
///
|
||||
/// - returns: All public keys within the given bundle.
|
||||
public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] {
|
||||
var publicKeys: [SecKey] = []
|
||||
|
||||
for certificate in certificates(in: bundle) {
|
||||
if let publicKey = publicKey(for: certificate) {
|
||||
publicKeys.append(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
return publicKeys
|
||||
}
|
||||
|
||||
// MARK: - Evaluation
|
||||
|
||||
/// Evaluates whether the server trust is valid for the given host.
|
||||
///
|
||||
/// - parameter serverTrust: The server trust to evaluate.
|
||||
/// - parameter host: The host of the challenge protection space.
|
||||
///
|
||||
/// - returns: Whether the server trust is valid.
|
||||
public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool {
|
||||
var serverTrustIsValid = false
|
||||
|
||||
switch self {
|
||||
case let .performDefaultEvaluation(validateHost):
|
||||
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
|
||||
SecTrustSetPolicies(serverTrust, policy)
|
||||
|
||||
serverTrustIsValid = trustIsValid(serverTrust)
|
||||
case let .performRevokedEvaluation(validateHost, revocationFlags):
|
||||
let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
|
||||
let revokedPolicy = SecPolicyCreateRevocation(revocationFlags)
|
||||
SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef)
|
||||
|
||||
serverTrustIsValid = trustIsValid(serverTrust)
|
||||
case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost):
|
||||
if validateCertificateChain {
|
||||
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
|
||||
SecTrustSetPolicies(serverTrust, policy)
|
||||
|
||||
SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray)
|
||||
SecTrustSetAnchorCertificatesOnly(serverTrust, true)
|
||||
|
||||
serverTrustIsValid = trustIsValid(serverTrust)
|
||||
} else {
|
||||
let serverCertificatesDataArray = certificateData(for: serverTrust)
|
||||
let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates)
|
||||
|
||||
outerLoop: for serverCertificateData in serverCertificatesDataArray {
|
||||
for pinnedCertificateData in pinnedCertificatesDataArray {
|
||||
if serverCertificateData == pinnedCertificateData {
|
||||
serverTrustIsValid = true
|
||||
break outerLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost):
|
||||
var certificateChainEvaluationPassed = true
|
||||
|
||||
if validateCertificateChain {
|
||||
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
|
||||
SecTrustSetPolicies(serverTrust, policy)
|
||||
|
||||
certificateChainEvaluationPassed = trustIsValid(serverTrust)
|
||||
}
|
||||
|
||||
if certificateChainEvaluationPassed {
|
||||
outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
|
||||
for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
|
||||
if serverPublicKey.isEqual(pinnedPublicKey) {
|
||||
serverTrustIsValid = true
|
||||
break outerLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .disableEvaluation:
|
||||
serverTrustIsValid = true
|
||||
case let .customEvaluation(closure):
|
||||
serverTrustIsValid = closure(serverTrust, host)
|
||||
}
|
||||
|
||||
return serverTrustIsValid
|
||||
}
|
||||
|
||||
// MARK: - Private - Trust Validation
|
||||
|
||||
private func trustIsValid(_ trust: SecTrust) -> Bool {
|
||||
var isValid = false
|
||||
|
||||
var result = SecTrustResultType.invalid
|
||||
let status = SecTrustEvaluate(trust, &result)
|
||||
|
||||
if status == errSecSuccess {
|
||||
let unspecified = SecTrustResultType.unspecified
|
||||
let proceed = SecTrustResultType.proceed
|
||||
|
||||
|
||||
isValid = result == unspecified || result == proceed
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// MARK: - Private - Certificate Data
|
||||
|
||||
private func certificateData(for trust: SecTrust) -> [Data] {
|
||||
var certificates: [SecCertificate] = []
|
||||
|
||||
for index in 0..<SecTrustGetCertificateCount(trust) {
|
||||
if let certificate = SecTrustGetCertificateAtIndex(trust, index) {
|
||||
certificates.append(certificate)
|
||||
}
|
||||
}
|
||||
|
||||
return certificateData(for: certificates)
|
||||
}
|
||||
|
||||
private func certificateData(for certificates: [SecCertificate]) -> [Data] {
|
||||
return certificates.map { SecCertificateCopyData($0) as Data }
|
||||
}
|
||||
|
||||
// MARK: - Private - Public Key Extraction
|
||||
|
||||
private static func publicKeys(for trust: SecTrust) -> [SecKey] {
|
||||
var publicKeys: [SecKey] = []
|
||||
|
||||
for index in 0..<SecTrustGetCertificateCount(trust) {
|
||||
if
|
||||
let certificate = SecTrustGetCertificateAtIndex(trust, index),
|
||||
let publicKey = publicKey(for: certificate)
|
||||
{
|
||||
publicKeys.append(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
return publicKeys
|
||||
}
|
||||
|
||||
private static func publicKey(for certificate: SecCertificate) -> SecKey? {
|
||||
var publicKey: SecKey?
|
||||
|
||||
let policy = SecPolicyCreateBasicX509()
|
||||
var trust: SecTrust?
|
||||
let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
|
||||
|
||||
if let trust = trust, trustCreationStatus == errSecSuccess {
|
||||
publicKey = SecTrustCopyPublicKey(trust)
|
||||
}
|
||||
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
725
Pods/Alamofire/Source/SessionDelegate.swift
generated
Normal file
725
Pods/Alamofire/Source/SessionDelegate.swift
generated
Normal file
@ -0,0 +1,725 @@
|
||||
//
|
||||
// SessionDelegate.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Responsible for handling all delegate callbacks for the underlying session.
|
||||
open class SessionDelegate: NSObject {
|
||||
|
||||
// MARK: URLSessionDelegate Overrides
|
||||
|
||||
/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didBecomeInvalidWithError:)`.
|
||||
open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
|
||||
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
|
||||
|
||||
/// Overrides all behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)` and requires the caller to call the `completionHandler`.
|
||||
open var sessionDidReceiveChallengeWithCompletion: ((URLSession, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
|
||||
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?
|
||||
|
||||
// MARK: URLSessionTaskDelegate Overrides
|
||||
|
||||
/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
|
||||
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
|
||||
|
||||
/// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` and
|
||||
/// requires the caller to call the `completionHandler`.
|
||||
open var taskWillPerformHTTPRedirectionWithCompletion: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)`.
|
||||
open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
|
||||
|
||||
/// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:didReceive:completionHandler:)` and
|
||||
/// requires the caller to call the `completionHandler`.
|
||||
open var taskDidReceiveChallengeWithCompletion: ((URLSession, URLSessionTask, URLAuthenticationChallenge, @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)`.
|
||||
open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
|
||||
|
||||
/// Overrides all behavior for URLSessionTaskDelegate method `urlSession(_:task:needNewBodyStream:)` and
|
||||
/// requires the caller to call the `completionHandler`.
|
||||
open var taskNeedNewBodyStreamWithCompletion: ((URLSession, URLSessionTask, @escaping (InputStream?) -> Void) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)`.
|
||||
open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:didCompleteWithError:)`.
|
||||
open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)?
|
||||
|
||||
// MARK: URLSessionDataDelegate Overrides
|
||||
|
||||
/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)`.
|
||||
open var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
|
||||
|
||||
/// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:completionHandler:)` and
|
||||
/// requires caller to call the `completionHandler`.
|
||||
open var dataTaskDidReceiveResponseWithCompletion: ((URLSession, URLSessionDataTask, URLResponse, @escaping (URLSession.ResponseDisposition) -> Void) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didBecome:)`.
|
||||
open var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:didReceive:)`.
|
||||
open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
|
||||
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
|
||||
|
||||
/// Overrides all behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)` and
|
||||
/// requires caller to call the `completionHandler`.
|
||||
open var dataTaskWillCacheResponseWithCompletion: ((URLSession, URLSessionDataTask, CachedURLResponse, @escaping (CachedURLResponse?) -> Void) -> Void)?
|
||||
|
||||
// MARK: URLSessionDownloadDelegate Overrides
|
||||
|
||||
/// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didFinishDownloadingTo:)`.
|
||||
open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)`.
|
||||
open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
|
||||
|
||||
/// Overrides default behavior for URLSessionDownloadDelegate method `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)`.
|
||||
open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
|
||||
|
||||
// MARK: URLSessionStreamDelegate Overrides
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
/// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:readClosedFor:)`.
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open var streamTaskReadClosed: ((URLSession, URLSessionStreamTask) -> Void)? {
|
||||
get {
|
||||
return _streamTaskReadClosed as? (URLSession, URLSessionStreamTask) -> Void
|
||||
}
|
||||
set {
|
||||
_streamTaskReadClosed = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:writeClosedFor:)`.
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open var streamTaskWriteClosed: ((URLSession, URLSessionStreamTask) -> Void)? {
|
||||
get {
|
||||
return _streamTaskWriteClosed as? (URLSession, URLSessionStreamTask) -> Void
|
||||
}
|
||||
set {
|
||||
_streamTaskWriteClosed = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:betterRouteDiscoveredFor:)`.
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open var streamTaskBetterRouteDiscovered: ((URLSession, URLSessionStreamTask) -> Void)? {
|
||||
get {
|
||||
return _streamTaskBetterRouteDiscovered as? (URLSession, URLSessionStreamTask) -> Void
|
||||
}
|
||||
set {
|
||||
_streamTaskBetterRouteDiscovered = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides default behavior for URLSessionStreamDelegate method `urlSession(_:streamTask:didBecome:outputStream:)`.
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open var streamTaskDidBecomeInputAndOutputStreams: ((URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void)? {
|
||||
get {
|
||||
return _streamTaskDidBecomeInputStream as? (URLSession, URLSessionStreamTask, InputStream, OutputStream) -> Void
|
||||
}
|
||||
set {
|
||||
_streamTaskDidBecomeInputStream = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var _streamTaskReadClosed: Any?
|
||||
var _streamTaskWriteClosed: Any?
|
||||
var _streamTaskBetterRouteDiscovered: Any?
|
||||
var _streamTaskDidBecomeInputStream: Any?
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var retrier: RequestRetrier?
|
||||
weak var sessionManager: SessionManager?
|
||||
|
||||
var requests: [Int: Request] = [:]
|
||||
private let lock = NSLock()
|
||||
|
||||
/// Access the task delegate for the specified task in a thread-safe manner.
|
||||
open subscript(task: URLSessionTask) -> Request? {
|
||||
get {
|
||||
lock.lock() ; defer { lock.unlock() }
|
||||
return requests[task.taskIdentifier]
|
||||
}
|
||||
set {
|
||||
lock.lock() ; defer { lock.unlock() }
|
||||
requests[task.taskIdentifier] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
/// Initializes the `SessionDelegate` instance.
|
||||
///
|
||||
/// - returns: The new `SessionDelegate` instance.
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: NSObject Overrides
|
||||
|
||||
/// Returns a `Bool` indicating whether the `SessionDelegate` implements or inherits a method that can respond
|
||||
/// to a specified message.
|
||||
///
|
||||
/// - parameter selector: A selector that identifies a message.
|
||||
///
|
||||
/// - returns: `true` if the receiver implements or inherits a method that can respond to selector, otherwise `false`.
|
||||
open override func responds(to selector: Selector) -> Bool {
|
||||
#if !os(macOS)
|
||||
if selector == #selector(URLSessionDelegate.urlSessionDidFinishEvents(forBackgroundURLSession:)) {
|
||||
return sessionDidFinishEventsForBackgroundURLSession != nil
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(watchOS)
|
||||
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
|
||||
switch selector {
|
||||
case #selector(URLSessionStreamDelegate.urlSession(_:readClosedFor:)):
|
||||
return streamTaskReadClosed != nil
|
||||
case #selector(URLSessionStreamDelegate.urlSession(_:writeClosedFor:)):
|
||||
return streamTaskWriteClosed != nil
|
||||
case #selector(URLSessionStreamDelegate.urlSession(_:betterRouteDiscoveredFor:)):
|
||||
return streamTaskBetterRouteDiscovered != nil
|
||||
case #selector(URLSessionStreamDelegate.urlSession(_:streamTask:didBecome:outputStream:)):
|
||||
return streamTaskDidBecomeInputAndOutputStreams != nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
switch selector {
|
||||
case #selector(URLSessionDelegate.urlSession(_:didBecomeInvalidWithError:)):
|
||||
return sessionDidBecomeInvalidWithError != nil
|
||||
case #selector(URLSessionDelegate.urlSession(_:didReceive:completionHandler:)):
|
||||
return (sessionDidReceiveChallenge != nil || sessionDidReceiveChallengeWithCompletion != nil)
|
||||
case #selector(URLSessionTaskDelegate.urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)):
|
||||
return (taskWillPerformHTTPRedirection != nil || taskWillPerformHTTPRedirectionWithCompletion != nil)
|
||||
case #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)):
|
||||
return (dataTaskDidReceiveResponse != nil || dataTaskDidReceiveResponseWithCompletion != nil)
|
||||
default:
|
||||
return type(of: self).instancesRespond(to: selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URLSessionDelegate
|
||||
|
||||
extension SessionDelegate: URLSessionDelegate {
|
||||
/// Tells the delegate that the session has been invalidated.
|
||||
///
|
||||
/// - parameter session: The session object that was invalidated.
|
||||
/// - parameter error: The error that caused invalidation, or nil if the invalidation was explicit.
|
||||
open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
|
||||
sessionDidBecomeInvalidWithError?(session, error)
|
||||
}
|
||||
|
||||
/// Requests credentials from the delegate in response to a session-level authentication request from the
|
||||
/// remote server.
|
||||
///
|
||||
/// - parameter session: The session containing the task that requested authentication.
|
||||
/// - parameter challenge: An object that contains the request for authentication.
|
||||
/// - parameter completionHandler: A handler that your delegate method must call providing the disposition
|
||||
/// and credential.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
guard sessionDidReceiveChallengeWithCompletion == nil else {
|
||||
sessionDidReceiveChallengeWithCompletion?(session, challenge, completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
|
||||
var credential: URLCredential?
|
||||
|
||||
if let sessionDidReceiveChallenge = sessionDidReceiveChallenge {
|
||||
(disposition, credential) = sessionDidReceiveChallenge(session, challenge)
|
||||
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
||||
let host = challenge.protectionSpace.host
|
||||
|
||||
if
|
||||
let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
|
||||
let serverTrust = challenge.protectionSpace.serverTrust
|
||||
{
|
||||
if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
|
||||
disposition = .useCredential
|
||||
credential = URLCredential(trust: serverTrust)
|
||||
} else {
|
||||
disposition = .cancelAuthenticationChallenge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(disposition, credential)
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
/// Tells the delegate that all messages enqueued for a session have been delivered.
|
||||
///
|
||||
/// - parameter session: The session that no longer has any outstanding requests.
|
||||
open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
||||
sessionDidFinishEventsForBackgroundURLSession?(session)
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - URLSessionTaskDelegate
|
||||
|
||||
extension SessionDelegate: URLSessionTaskDelegate {
|
||||
/// Tells the delegate that the remote server requested an HTTP redirect.
|
||||
///
|
||||
/// - parameter session: The session containing the task whose request resulted in a redirect.
|
||||
/// - parameter task: The task whose request resulted in a redirect.
|
||||
/// - parameter response: An object containing the server’s response to the original request.
|
||||
/// - parameter request: A URL request object filled out with the new location.
|
||||
/// - parameter completionHandler: A closure that your handler should call with either the value of the request
|
||||
/// parameter, a modified URL request object, or NULL to refuse the redirect and
|
||||
/// return the body of the redirect response.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
willPerformHTTPRedirection response: HTTPURLResponse,
|
||||
newRequest request: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void)
|
||||
{
|
||||
guard taskWillPerformHTTPRedirectionWithCompletion == nil else {
|
||||
taskWillPerformHTTPRedirectionWithCompletion?(session, task, response, request, completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
var redirectRequest: URLRequest? = request
|
||||
|
||||
if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
|
||||
redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
|
||||
}
|
||||
|
||||
completionHandler(redirectRequest)
|
||||
}
|
||||
|
||||
/// Requests credentials from the delegate in response to an authentication request from the remote server.
|
||||
///
|
||||
/// - parameter session: The session containing the task whose request requires authentication.
|
||||
/// - parameter task: The task whose request requires authentication.
|
||||
/// - parameter challenge: An object that contains the request for authentication.
|
||||
/// - parameter completionHandler: A handler that your delegate method must call providing the disposition
|
||||
/// and credential.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
guard taskDidReceiveChallengeWithCompletion == nil else {
|
||||
taskDidReceiveChallengeWithCompletion?(session, task, challenge, completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
if let taskDidReceiveChallenge = taskDidReceiveChallenge {
|
||||
let result = taskDidReceiveChallenge(session, task, challenge)
|
||||
completionHandler(result.0, result.1)
|
||||
} else if let delegate = self[task]?.delegate {
|
||||
delegate.urlSession(
|
||||
session,
|
||||
task: task,
|
||||
didReceive: challenge,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
} else {
|
||||
urlSession(session, didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the delegate when a task requires a new request body stream to send to the remote server.
|
||||
///
|
||||
/// - parameter session: The session containing the task that needs a new body stream.
|
||||
/// - parameter task: The task that needs a new body stream.
|
||||
/// - parameter completionHandler: A completion handler that your delegate method should call with the new body stream.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
|
||||
{
|
||||
guard taskNeedNewBodyStreamWithCompletion == nil else {
|
||||
taskNeedNewBodyStreamWithCompletion?(session, task, completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
if let taskNeedNewBodyStream = taskNeedNewBodyStream {
|
||||
completionHandler(taskNeedNewBodyStream(session, task))
|
||||
} else if let delegate = self[task]?.delegate {
|
||||
delegate.urlSession(session, task: task, needNewBodyStream: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodically informs the delegate of the progress of sending body content to the server.
|
||||
///
|
||||
/// - parameter session: The session containing the data task.
|
||||
/// - parameter task: The data task.
|
||||
/// - parameter bytesSent: The number of bytes sent since the last time this delegate method was called.
|
||||
/// - parameter totalBytesSent: The total number of bytes sent so far.
|
||||
/// - parameter totalBytesExpectedToSend: The expected length of the body data.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didSendBodyData bytesSent: Int64,
|
||||
totalBytesSent: Int64,
|
||||
totalBytesExpectedToSend: Int64)
|
||||
{
|
||||
if let taskDidSendBodyData = taskDidSendBodyData {
|
||||
taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
|
||||
} else if let delegate = self[task]?.delegate as? UploadTaskDelegate {
|
||||
delegate.URLSession(
|
||||
session,
|
||||
task: task,
|
||||
didSendBodyData: bytesSent,
|
||||
totalBytesSent: totalBytesSent,
|
||||
totalBytesExpectedToSend: totalBytesExpectedToSend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
/// Tells the delegate that the session finished collecting metrics for the task.
|
||||
///
|
||||
/// - parameter session: The session collecting the metrics.
|
||||
/// - parameter task: The task whose metrics have been collected.
|
||||
/// - parameter metrics: The collected metrics.
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
@objc(URLSession:task:didFinishCollectingMetrics:)
|
||||
open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
|
||||
self[task]?.delegate.metrics = metrics
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// Tells the delegate that the task finished transferring data.
|
||||
///
|
||||
/// - parameter session: The session containing the task whose request finished transferring data.
|
||||
/// - parameter task: The task whose request finished transferring data.
|
||||
/// - parameter error: If an error occurred, an error object indicating how the transfer failed, otherwise nil.
|
||||
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
/// Executed after it is determined that the request is not going to be retried
|
||||
let completeTask: (URLSession, URLSessionTask, Error?) -> Void = { [weak self] session, task, error in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
strongSelf.taskDidComplete?(session, task, error)
|
||||
|
||||
strongSelf[task]?.delegate.urlSession(session, task: task, didCompleteWithError: error)
|
||||
|
||||
var userInfo: [String: Any] = [Notification.Key.Task: task]
|
||||
|
||||
if let data = (strongSelf[task]?.delegate as? DataTaskDelegate)?.data {
|
||||
userInfo[Notification.Key.ResponseData] = data
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name.Task.DidComplete,
|
||||
object: strongSelf,
|
||||
userInfo: userInfo
|
||||
)
|
||||
|
||||
strongSelf[task] = nil
|
||||
}
|
||||
|
||||
guard let request = self[task], let sessionManager = sessionManager else {
|
||||
completeTask(session, task, error)
|
||||
return
|
||||
}
|
||||
|
||||
// Run all validations on the request before checking if an error occurred
|
||||
request.validations.forEach { $0() }
|
||||
|
||||
// Determine whether an error has occurred
|
||||
var error: Error? = error
|
||||
|
||||
if request.delegate.error != nil {
|
||||
error = request.delegate.error
|
||||
}
|
||||
|
||||
/// If an error occurred and the retrier is set, asynchronously ask the retrier if the request
|
||||
/// should be retried. Otherwise, complete the task by notifying the task delegate.
|
||||
if let retrier = retrier, let error = error {
|
||||
retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in
|
||||
guard shouldRetry else { completeTask(session, task, error) ; return }
|
||||
|
||||
DispatchQueue.utility.after(timeDelay) { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
let retrySucceeded = strongSelf.sessionManager?.retry(request) ?? false
|
||||
|
||||
if retrySucceeded, let task = request.task {
|
||||
strongSelf[task] = request
|
||||
return
|
||||
} else {
|
||||
completeTask(session, task, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completeTask(session, task, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URLSessionDataDelegate
|
||||
|
||||
extension SessionDelegate: URLSessionDataDelegate {
|
||||
/// Tells the delegate that the data task received the initial reply (headers) from the server.
|
||||
///
|
||||
/// - parameter session: The session containing the data task that received an initial reply.
|
||||
/// - parameter dataTask: The data task that received an initial reply.
|
||||
/// - parameter response: A URL response object populated with headers.
|
||||
/// - parameter completionHandler: A completion handler that your code calls to continue the transfer, passing a
|
||||
/// constant to indicate whether the transfer should continue as a data task or
|
||||
/// should become a download task.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didReceive response: URLResponse,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
|
||||
{
|
||||
guard dataTaskDidReceiveResponseWithCompletion == nil else {
|
||||
dataTaskDidReceiveResponseWithCompletion?(session, dataTask, response, completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
var disposition: URLSession.ResponseDisposition = .allow
|
||||
|
||||
if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
|
||||
disposition = dataTaskDidReceiveResponse(session, dataTask, response)
|
||||
}
|
||||
|
||||
completionHandler(disposition)
|
||||
}
|
||||
|
||||
/// Tells the delegate that the data task was changed to a download task.
|
||||
///
|
||||
/// - parameter session: The session containing the task that was replaced by a download task.
|
||||
/// - parameter dataTask: The data task that was replaced by a download task.
|
||||
/// - parameter downloadTask: The new download task that replaced the data task.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didBecome downloadTask: URLSessionDownloadTask)
|
||||
{
|
||||
if let dataTaskDidBecomeDownloadTask = dataTaskDidBecomeDownloadTask {
|
||||
dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask)
|
||||
} else {
|
||||
self[downloadTask]?.delegate = DownloadTaskDelegate(task: downloadTask)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the delegate that the data task has received some of the expected data.
|
||||
///
|
||||
/// - parameter session: The session containing the data task that provided data.
|
||||
/// - parameter dataTask: The data task that provided data.
|
||||
/// - parameter data: A data object containing the transferred data.
|
||||
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
if let dataTaskDidReceiveData = dataTaskDidReceiveData {
|
||||
dataTaskDidReceiveData(session, dataTask, data)
|
||||
} else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {
|
||||
delegate.urlSession(session, dataTask: dataTask, didReceive: data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the delegate whether the data (or upload) task should store the response in the cache.
|
||||
///
|
||||
/// - parameter session: The session containing the data (or upload) task.
|
||||
/// - parameter dataTask: The data (or upload) task.
|
||||
/// - parameter proposedResponse: The default caching behavior. This behavior is determined based on the current
|
||||
/// caching policy and the values of certain received headers, such as the Pragma
|
||||
/// and Cache-Control headers.
|
||||
/// - parameter completionHandler: A block that your handler must call, providing either the original proposed
|
||||
/// response, a modified version of that response, or NULL to prevent caching the
|
||||
/// response. If your delegate implements this method, it must call this completion
|
||||
/// handler; otherwise, your app leaks memory.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
willCacheResponse proposedResponse: CachedURLResponse,
|
||||
completionHandler: @escaping (CachedURLResponse?) -> Void)
|
||||
{
|
||||
guard dataTaskWillCacheResponseWithCompletion == nil else {
|
||||
dataTaskWillCacheResponseWithCompletion?(session, dataTask, proposedResponse, completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
|
||||
completionHandler(dataTaskWillCacheResponse(session, dataTask, proposedResponse))
|
||||
} else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {
|
||||
delegate.urlSession(
|
||||
session,
|
||||
dataTask: dataTask,
|
||||
willCacheResponse: proposedResponse,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
} else {
|
||||
completionHandler(proposedResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URLSessionDownloadDelegate
|
||||
|
||||
extension SessionDelegate: URLSessionDownloadDelegate {
|
||||
/// Tells the delegate that a download task has finished downloading.
|
||||
///
|
||||
/// - parameter session: The session containing the download task that finished.
|
||||
/// - parameter downloadTask: The download task that finished.
|
||||
/// - parameter location: A file URL for the temporary file. Because the file is temporary, you must either
|
||||
/// open the file for reading or move it to a permanent location in your app’s sandbox
|
||||
/// container directory before returning from this delegate method.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didFinishDownloadingTo location: URL)
|
||||
{
|
||||
if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
|
||||
downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
|
||||
} else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
|
||||
delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodically informs the delegate about the download’s progress.
|
||||
///
|
||||
/// - parameter session: The session containing the download task.
|
||||
/// - parameter downloadTask: The download task.
|
||||
/// - parameter bytesWritten: The number of bytes transferred since the last time this delegate
|
||||
/// method was called.
|
||||
/// - parameter totalBytesWritten: The total number of bytes transferred so far.
|
||||
/// - parameter totalBytesExpectedToWrite: The expected length of the file, as provided by the Content-Length
|
||||
/// header. If this header was not provided, the value is
|
||||
/// `NSURLSessionTransferSizeUnknown`.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64)
|
||||
{
|
||||
if let downloadTaskDidWriteData = downloadTaskDidWriteData {
|
||||
downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
} else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
|
||||
delegate.urlSession(
|
||||
session,
|
||||
downloadTask: downloadTask,
|
||||
didWriteData: bytesWritten,
|
||||
totalBytesWritten: totalBytesWritten,
|
||||
totalBytesExpectedToWrite: totalBytesExpectedToWrite
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the delegate that the download task has resumed downloading.
|
||||
///
|
||||
/// - parameter session: The session containing the download task that finished.
|
||||
/// - parameter downloadTask: The download task that resumed. See explanation in the discussion.
|
||||
/// - parameter fileOffset: If the file's cache policy or last modified date prevents reuse of the
|
||||
/// existing content, then this value is zero. Otherwise, this value is an
|
||||
/// integer representing the number of bytes on disk that do not need to be
|
||||
/// retrieved again.
|
||||
/// - parameter expectedTotalBytes: The expected length of the file, as provided by the Content-Length header.
|
||||
/// If this header was not provided, the value is NSURLSessionTransferSizeUnknown.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didResumeAtOffset fileOffset: Int64,
|
||||
expectedTotalBytes: Int64)
|
||||
{
|
||||
if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
|
||||
downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
|
||||
} else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
|
||||
delegate.urlSession(
|
||||
session,
|
||||
downloadTask: downloadTask,
|
||||
didResumeAtOffset: fileOffset,
|
||||
expectedTotalBytes: expectedTotalBytes
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URLSessionStreamDelegate
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
extension SessionDelegate: URLSessionStreamDelegate {
|
||||
/// Tells the delegate that the read side of the connection has been closed.
|
||||
///
|
||||
/// - parameter session: The session.
|
||||
/// - parameter streamTask: The stream task.
|
||||
open func urlSession(_ session: URLSession, readClosedFor streamTask: URLSessionStreamTask) {
|
||||
streamTaskReadClosed?(session, streamTask)
|
||||
}
|
||||
|
||||
/// Tells the delegate that the write side of the connection has been closed.
|
||||
///
|
||||
/// - parameter session: The session.
|
||||
/// - parameter streamTask: The stream task.
|
||||
open func urlSession(_ session: URLSession, writeClosedFor streamTask: URLSessionStreamTask) {
|
||||
streamTaskWriteClosed?(session, streamTask)
|
||||
}
|
||||
|
||||
/// Tells the delegate that the system has determined that a better route to the host is available.
|
||||
///
|
||||
/// - parameter session: The session.
|
||||
/// - parameter streamTask: The stream task.
|
||||
open func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: URLSessionStreamTask) {
|
||||
streamTaskBetterRouteDiscovered?(session, streamTask)
|
||||
}
|
||||
|
||||
/// Tells the delegate that the stream task has been completed and provides the unopened stream objects.
|
||||
///
|
||||
/// - parameter session: The session.
|
||||
/// - parameter streamTask: The stream task.
|
||||
/// - parameter inputStream: The new input stream.
|
||||
/// - parameter outputStream: The new output stream.
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
streamTask: URLSessionStreamTask,
|
||||
didBecome inputStream: InputStream,
|
||||
outputStream: OutputStream)
|
||||
{
|
||||
streamTaskDidBecomeInputAndOutputStreams?(session, streamTask, inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
896
Pods/Alamofire/Source/SessionManager.swift
generated
Normal file
896
Pods/Alamofire/Source/SessionManager.swift
generated
Normal file
@ -0,0 +1,896 @@
|
||||
//
|
||||
// SessionManager.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Responsible for creating and managing `Request` objects, as well as their underlying `NSURLSession`.
|
||||
open class SessionManager {
|
||||
|
||||
// MARK: - Helper Types
|
||||
|
||||
/// Defines whether the `MultipartFormData` encoding was successful and contains result of the encoding as
|
||||
/// associated values.
|
||||
///
|
||||
/// - Success: Represents a successful `MultipartFormData` encoding and contains the new `UploadRequest` along with
|
||||
/// streaming information.
|
||||
/// - Failure: Used to represent a failure in the `MultipartFormData` encoding and also contains the encoding
|
||||
/// error.
|
||||
public enum MultipartFormDataEncodingResult {
|
||||
case success(request: UploadRequest, streamingFromDisk: Bool, streamFileURL: URL?)
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// A default instance of `SessionManager`, used by top-level Alamofire request methods, and suitable for use
|
||||
/// directly for any ad hoc requests.
|
||||
public static let `default`: SessionManager = {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
|
||||
|
||||
return SessionManager(configuration: configuration)
|
||||
}()
|
||||
|
||||
/// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers.
|
||||
public static let defaultHTTPHeaders: HTTPHeaders = {
|
||||
// Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3
|
||||
let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5"
|
||||
|
||||
// Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5
|
||||
let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in
|
||||
let quality = 1.0 - (Double(index) * 0.1)
|
||||
return "\(languageCode);q=\(quality)"
|
||||
}.joined(separator: ", ")
|
||||
|
||||
// User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3
|
||||
// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0`
|
||||
let userAgent: String = {
|
||||
if let info = Bundle.main.infoDictionary {
|
||||
let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown"
|
||||
let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
|
||||
let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown"
|
||||
let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown"
|
||||
|
||||
let osNameVersion: String = {
|
||||
let version = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
|
||||
|
||||
let osName: String = {
|
||||
#if os(iOS)
|
||||
return "iOS"
|
||||
#elseif os(watchOS)
|
||||
return "watchOS"
|
||||
#elseif os(tvOS)
|
||||
return "tvOS"
|
||||
#elseif os(macOS)
|
||||
return "OS X"
|
||||
#elseif os(Linux)
|
||||
return "Linux"
|
||||
#else
|
||||
return "Unknown"
|
||||
#endif
|
||||
}()
|
||||
|
||||
return "\(osName) \(versionString)"
|
||||
}()
|
||||
|
||||
let alamofireVersion: String = {
|
||||
guard
|
||||
let afInfo = Bundle(for: SessionManager.self).infoDictionary,
|
||||
let build = afInfo["CFBundleShortVersionString"]
|
||||
else { return "Unknown" }
|
||||
|
||||
return "Alamofire/\(build)"
|
||||
}()
|
||||
|
||||
return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
|
||||
}
|
||||
|
||||
return "Alamofire"
|
||||
}()
|
||||
|
||||
return [
|
||||
"Accept-Encoding": acceptEncoding,
|
||||
"Accept-Language": acceptLanguage,
|
||||
"User-Agent": userAgent
|
||||
]
|
||||
}()
|
||||
|
||||
/// Default memory threshold used when encoding `MultipartFormData` in bytes.
|
||||
public static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000
|
||||
|
||||
/// The underlying session.
|
||||
public let session: URLSession
|
||||
|
||||
/// The session delegate handling all the task and session delegate callbacks.
|
||||
public let delegate: SessionDelegate
|
||||
|
||||
/// Whether to start requests immediately after being constructed. `true` by default.
|
||||
open var startRequestsImmediately: Bool = true
|
||||
|
||||
/// The request adapter called each time a new request is created.
|
||||
open var adapter: RequestAdapter?
|
||||
|
||||
/// The request retrier called each time a request encounters an error to determine whether to retry the request.
|
||||
open var retrier: RequestRetrier? {
|
||||
get { return delegate.retrier }
|
||||
set { delegate.retrier = newValue }
|
||||
}
|
||||
|
||||
/// The background completion handler closure provided by the UIApplicationDelegate
|
||||
/// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background
|
||||
/// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation
|
||||
/// will automatically call the handler.
|
||||
///
|
||||
/// If you need to handle your own events before the handler is called, then you need to override the
|
||||
/// SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` and manually call the handler when finished.
|
||||
///
|
||||
/// `nil` by default.
|
||||
open var backgroundCompletionHandler: (() -> Void)?
|
||||
|
||||
let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Creates an instance with the specified `configuration`, `delegate` and `serverTrustPolicyManager`.
|
||||
///
|
||||
/// - parameter configuration: The configuration used to construct the managed session.
|
||||
/// `URLSessionConfiguration.default` by default.
|
||||
/// - parameter delegate: The delegate used when initializing the session. `SessionDelegate()` by
|
||||
/// default.
|
||||
/// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust
|
||||
/// challenges. `nil` by default.
|
||||
///
|
||||
/// - returns: The new `SessionManager` instance.
|
||||
public init(
|
||||
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
|
||||
delegate: SessionDelegate = SessionDelegate(),
|
||||
serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
|
||||
{
|
||||
self.delegate = delegate
|
||||
self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
|
||||
|
||||
commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
|
||||
}
|
||||
|
||||
/// Creates an instance with the specified `session`, `delegate` and `serverTrustPolicyManager`.
|
||||
///
|
||||
/// - parameter session: The URL session.
|
||||
/// - parameter delegate: The delegate of the URL session. Must equal the URL session's delegate.
|
||||
/// - parameter serverTrustPolicyManager: The server trust policy manager to use for evaluating all server trust
|
||||
/// challenges. `nil` by default.
|
||||
///
|
||||
/// - returns: The new `SessionManager` instance if the URL session's delegate matches; `nil` otherwise.
|
||||
public init?(
|
||||
session: URLSession,
|
||||
delegate: SessionDelegate,
|
||||
serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
|
||||
{
|
||||
guard delegate === session.delegate else { return nil }
|
||||
|
||||
self.delegate = delegate
|
||||
self.session = session
|
||||
|
||||
commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
|
||||
}
|
||||
|
||||
private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) {
|
||||
session.serverTrustPolicyManager = serverTrustPolicyManager
|
||||
|
||||
delegate.sessionManager = self
|
||||
|
||||
delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
|
||||
guard let strongSelf = self else { return }
|
||||
DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
session.invalidateAndCancel()
|
||||
}
|
||||
|
||||
// MARK: - Data Request
|
||||
|
||||
/// Creates a `DataRequest` to retrieve the contents of the specified `url`, `method`, `parameters`, `encoding`
|
||||
/// and `headers`.
|
||||
///
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.get` by default.
|
||||
/// - parameter parameters: The parameters. `nil` by default.
|
||||
/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DataRequest`.
|
||||
@discardableResult
|
||||
open func request(
|
||||
_ url: URLConvertible,
|
||||
method: HTTPMethod = .get,
|
||||
parameters: Parameters? = nil,
|
||||
encoding: ParameterEncoding = URLEncoding.default,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> DataRequest
|
||||
{
|
||||
var originalRequest: URLRequest?
|
||||
|
||||
do {
|
||||
originalRequest = try URLRequest(url: url, method: method, headers: headers)
|
||||
let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
|
||||
return request(encodedURLRequest)
|
||||
} catch {
|
||||
return request(originalRequest, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `DataRequest` to retrieve the contents of a URL based on the specified `urlRequest`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request.
|
||||
///
|
||||
/// - returns: The created `DataRequest`.
|
||||
@discardableResult
|
||||
open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
|
||||
var originalRequest: URLRequest?
|
||||
|
||||
do {
|
||||
originalRequest = try urlRequest.asURLRequest()
|
||||
let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)
|
||||
|
||||
let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
|
||||
let request = DataRequest(session: session, requestTask: .data(originalTask, task))
|
||||
|
||||
delegate[task] = request
|
||||
|
||||
if startRequestsImmediately { request.resume() }
|
||||
|
||||
return request
|
||||
} catch {
|
||||
return request(originalRequest, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private - Request Implementation
|
||||
|
||||
private func request(_ urlRequest: URLRequest?, failedWith error: Error) -> DataRequest {
|
||||
var requestTask: Request.RequestTask = .data(nil, nil)
|
||||
|
||||
if let urlRequest = urlRequest {
|
||||
let originalTask = DataRequest.Requestable(urlRequest: urlRequest)
|
||||
requestTask = .data(originalTask, nil)
|
||||
}
|
||||
|
||||
let underlyingError = error.underlyingAdaptError ?? error
|
||||
let request = DataRequest(session: session, requestTask: requestTask, error: underlyingError)
|
||||
|
||||
if let retrier = retrier, error is AdaptError {
|
||||
allowRetrier(retrier, toRetry: request, with: underlyingError)
|
||||
} else {
|
||||
if startRequestsImmediately { request.resume() }
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
// MARK: - Download Request
|
||||
|
||||
// MARK: URL Request
|
||||
|
||||
/// Creates a `DownloadRequest` to retrieve the contents the specified `url`, `method`, `parameters`, `encoding`,
|
||||
/// `headers` and save them to the `destination`.
|
||||
///
|
||||
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
|
||||
/// underlying URL session.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.get` by default.
|
||||
/// - parameter parameters: The parameters. `nil` by default.
|
||||
/// - parameter encoding: The parameter encoding. `URLEncoding.default` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DownloadRequest`.
|
||||
@discardableResult
|
||||
open func download(
|
||||
_ url: URLConvertible,
|
||||
method: HTTPMethod = .get,
|
||||
parameters: Parameters? = nil,
|
||||
encoding: ParameterEncoding = URLEncoding.default,
|
||||
headers: HTTPHeaders? = nil,
|
||||
to destination: DownloadRequest.DownloadFileDestination? = nil)
|
||||
-> DownloadRequest
|
||||
{
|
||||
do {
|
||||
let urlRequest = try URLRequest(url: url, method: method, headers: headers)
|
||||
let encodedURLRequest = try encoding.encode(urlRequest, with: parameters)
|
||||
return download(encodedURLRequest, to: destination)
|
||||
} catch {
|
||||
return download(nil, to: destination, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `DownloadRequest` to retrieve the contents of a URL based on the specified `urlRequest` and save
|
||||
/// them to the `destination`.
|
||||
///
|
||||
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
|
||||
/// underlying URL session.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request
|
||||
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DownloadRequest`.
|
||||
@discardableResult
|
||||
open func download(
|
||||
_ urlRequest: URLRequestConvertible,
|
||||
to destination: DownloadRequest.DownloadFileDestination? = nil)
|
||||
-> DownloadRequest
|
||||
{
|
||||
do {
|
||||
let urlRequest = try urlRequest.asURLRequest()
|
||||
return download(.request(urlRequest), to: destination)
|
||||
} catch {
|
||||
return download(nil, to: destination, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Resume Data
|
||||
|
||||
/// Creates a `DownloadRequest` from the `resumeData` produced from a previous request cancellation to retrieve
|
||||
/// the contents of the original request and save them to the `destination`.
|
||||
///
|
||||
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
|
||||
/// underlying URL session.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken
|
||||
/// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the
|
||||
/// data is written incorrectly and will always fail to resume the download. For more information about the bug and
|
||||
/// possible workarounds, please refer to the following Stack Overflow post:
|
||||
///
|
||||
/// - http://stackoverflow.com/a/39347461/1342462
|
||||
///
|
||||
/// - parameter resumeData: The resume data. This is an opaque data blob produced by `URLSessionDownloadTask`
|
||||
/// when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for
|
||||
/// additional information.
|
||||
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `DownloadRequest`.
|
||||
@discardableResult
|
||||
open func download(
|
||||
resumingWith resumeData: Data,
|
||||
to destination: DownloadRequest.DownloadFileDestination? = nil)
|
||||
-> DownloadRequest
|
||||
{
|
||||
return download(.resumeData(resumeData), to: destination)
|
||||
}
|
||||
|
||||
// MARK: Private - Download Implementation
|
||||
|
||||
private func download(
|
||||
_ downloadable: DownloadRequest.Downloadable,
|
||||
to destination: DownloadRequest.DownloadFileDestination?)
|
||||
-> DownloadRequest
|
||||
{
|
||||
do {
|
||||
let task = try downloadable.task(session: session, adapter: adapter, queue: queue)
|
||||
let download = DownloadRequest(session: session, requestTask: .download(downloadable, task))
|
||||
|
||||
download.downloadDelegate.destination = destination
|
||||
|
||||
delegate[task] = download
|
||||
|
||||
if startRequestsImmediately { download.resume() }
|
||||
|
||||
return download
|
||||
} catch {
|
||||
return download(downloadable, to: destination, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func download(
|
||||
_ downloadable: DownloadRequest.Downloadable?,
|
||||
to destination: DownloadRequest.DownloadFileDestination?,
|
||||
failedWith error: Error)
|
||||
-> DownloadRequest
|
||||
{
|
||||
var downloadTask: Request.RequestTask = .download(nil, nil)
|
||||
|
||||
if let downloadable = downloadable {
|
||||
downloadTask = .download(downloadable, nil)
|
||||
}
|
||||
|
||||
let underlyingError = error.underlyingAdaptError ?? error
|
||||
|
||||
let download = DownloadRequest(session: session, requestTask: downloadTask, error: underlyingError)
|
||||
download.downloadDelegate.destination = destination
|
||||
|
||||
if let retrier = retrier, error is AdaptError {
|
||||
allowRetrier(retrier, toRetry: download, with: underlyingError)
|
||||
} else {
|
||||
if startRequestsImmediately { download.resume() }
|
||||
}
|
||||
|
||||
return download
|
||||
}
|
||||
|
||||
// MARK: - Upload Request
|
||||
|
||||
// MARK: File
|
||||
|
||||
/// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `file`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter file: The file to upload.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
open func upload(
|
||||
_ fileURL: URL,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> UploadRequest
|
||||
{
|
||||
do {
|
||||
let urlRequest = try URLRequest(url: url, method: method, headers: headers)
|
||||
return upload(fileURL, with: urlRequest)
|
||||
} catch {
|
||||
return upload(nil, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `UploadRequest` from the specified `urlRequest` for uploading the `file`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter file: The file to upload.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
open func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest {
|
||||
do {
|
||||
let urlRequest = try urlRequest.asURLRequest()
|
||||
return upload(.file(fileURL, urlRequest))
|
||||
} catch {
|
||||
return upload(nil, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Data
|
||||
|
||||
/// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `data`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter data: The data to upload.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
open func upload(
|
||||
_ data: Data,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> UploadRequest
|
||||
{
|
||||
do {
|
||||
let urlRequest = try URLRequest(url: url, method: method, headers: headers)
|
||||
return upload(data, with: urlRequest)
|
||||
} catch {
|
||||
return upload(nil, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `data`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter data: The data to upload.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
open func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
|
||||
do {
|
||||
let urlRequest = try urlRequest.asURLRequest()
|
||||
return upload(.data(data, urlRequest))
|
||||
} catch {
|
||||
return upload(nil, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: InputStream
|
||||
|
||||
/// Creates an `UploadRequest` from the specified `url`, `method` and `headers` for uploading the `stream`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter stream: The stream to upload.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
open func upload(
|
||||
_ stream: InputStream,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil)
|
||||
-> UploadRequest
|
||||
{
|
||||
do {
|
||||
let urlRequest = try URLRequest(url: url, method: method, headers: headers)
|
||||
return upload(stream, with: urlRequest)
|
||||
} catch {
|
||||
return upload(nil, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `UploadRequest` from the specified `urlRequest` for uploading the `stream`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter stream: The stream to upload.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
///
|
||||
/// - returns: The created `UploadRequest`.
|
||||
@discardableResult
|
||||
open func upload(_ stream: InputStream, with urlRequest: URLRequestConvertible) -> UploadRequest {
|
||||
do {
|
||||
let urlRequest = try urlRequest.asURLRequest()
|
||||
return upload(.stream(stream, urlRequest))
|
||||
} catch {
|
||||
return upload(nil, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MultipartFormData
|
||||
|
||||
/// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new
|
||||
/// `UploadRequest` using the `url`, `method` and `headers`.
|
||||
///
|
||||
/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
|
||||
/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
|
||||
/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
|
||||
/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
|
||||
/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
|
||||
/// used for larger payloads such as video content.
|
||||
///
|
||||
/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
|
||||
/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
|
||||
/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
|
||||
/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
|
||||
/// technique was used.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`.
|
||||
/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
|
||||
/// `multipartFormDataEncodingMemoryThreshold` by default.
|
||||
/// - parameter url: The URL.
|
||||
/// - parameter method: The HTTP method. `.post` by default.
|
||||
/// - parameter headers: The HTTP headers. `nil` by default.
|
||||
/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete.
|
||||
open func upload(
|
||||
multipartFormData: @escaping (MultipartFormData) -> Void,
|
||||
usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
|
||||
to url: URLConvertible,
|
||||
method: HTTPMethod = .post,
|
||||
headers: HTTPHeaders? = nil,
|
||||
encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?)
|
||||
{
|
||||
do {
|
||||
let urlRequest = try URLRequest(url: url, method: method, headers: headers)
|
||||
|
||||
return upload(
|
||||
multipartFormData: multipartFormData,
|
||||
usingThreshold: encodingMemoryThreshold,
|
||||
with: urlRequest,
|
||||
encodingCompletion: encodingCompletion
|
||||
)
|
||||
} catch {
|
||||
DispatchQueue.main.async { encodingCompletion?(.failure(error)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes `multipartFormData` using `encodingMemoryThreshold` and calls `encodingCompletion` with new
|
||||
/// `UploadRequest` using the `urlRequest`.
|
||||
///
|
||||
/// It is important to understand the memory implications of uploading `MultipartFormData`. If the cummulative
|
||||
/// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most
|
||||
/// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to
|
||||
/// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory
|
||||
/// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be
|
||||
/// used for larger payloads such as video content.
|
||||
///
|
||||
/// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory
|
||||
/// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`,
|
||||
/// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk
|
||||
/// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding
|
||||
/// technique was used.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter multipartFormData: The closure used to append body parts to the `MultipartFormData`.
|
||||
/// - parameter encodingMemoryThreshold: The encoding memory threshold in bytes.
|
||||
/// `multipartFormDataEncodingMemoryThreshold` by default.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter encodingCompletion: The closure called when the `MultipartFormData` encoding is complete.
|
||||
open func upload(
|
||||
multipartFormData: @escaping (MultipartFormData) -> Void,
|
||||
usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
|
||||
with urlRequest: URLRequestConvertible,
|
||||
encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?)
|
||||
{
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let formData = MultipartFormData()
|
||||
multipartFormData(formData)
|
||||
|
||||
var tempFileURL: URL?
|
||||
|
||||
do {
|
||||
var urlRequestWithContentType = try urlRequest.asURLRequest()
|
||||
urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let isBackgroundSession = self.session.configuration.identifier != nil
|
||||
|
||||
if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession {
|
||||
let data = try formData.encode()
|
||||
|
||||
let encodingResult = MultipartFormDataEncodingResult.success(
|
||||
request: self.upload(data, with: urlRequestWithContentType),
|
||||
streamingFromDisk: false,
|
||||
streamFileURL: nil
|
||||
)
|
||||
|
||||
DispatchQueue.main.async { encodingCompletion?(encodingResult) }
|
||||
} else {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
|
||||
let fileName = UUID().uuidString
|
||||
let fileURL = directoryURL.appendingPathComponent(fileName)
|
||||
|
||||
tempFileURL = fileURL
|
||||
|
||||
var directoryError: Error?
|
||||
|
||||
// Create directory inside serial queue to ensure two threads don't do this in parallel
|
||||
self.queue.sync {
|
||||
do {
|
||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
directoryError = error
|
||||
}
|
||||
}
|
||||
|
||||
if let directoryError = directoryError { throw directoryError }
|
||||
|
||||
try formData.writeEncodedData(to: fileURL)
|
||||
|
||||
let upload = self.upload(fileURL, with: urlRequestWithContentType)
|
||||
|
||||
// Cleanup the temp file once the upload is complete
|
||||
upload.delegate.queue.addOperation {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let encodingResult = MultipartFormDataEncodingResult.success(
|
||||
request: upload,
|
||||
streamingFromDisk: true,
|
||||
streamFileURL: fileURL
|
||||
)
|
||||
|
||||
encodingCompletion?(encodingResult)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Cleanup the temp file in the event that the multipart form data encoding failed
|
||||
if let tempFileURL = tempFileURL {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: tempFileURL)
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { encodingCompletion?(.failure(error)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private - Upload Implementation
|
||||
|
||||
private func upload(_ uploadable: UploadRequest.Uploadable) -> UploadRequest {
|
||||
do {
|
||||
let task = try uploadable.task(session: session, adapter: adapter, queue: queue)
|
||||
let upload = UploadRequest(session: session, requestTask: .upload(uploadable, task))
|
||||
|
||||
if case let .stream(inputStream, _) = uploadable {
|
||||
upload.delegate.taskNeedNewBodyStream = { _, _ in inputStream }
|
||||
}
|
||||
|
||||
delegate[task] = upload
|
||||
|
||||
if startRequestsImmediately { upload.resume() }
|
||||
|
||||
return upload
|
||||
} catch {
|
||||
return upload(uploadable, failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func upload(_ uploadable: UploadRequest.Uploadable?, failedWith error: Error) -> UploadRequest {
|
||||
var uploadTask: Request.RequestTask = .upload(nil, nil)
|
||||
|
||||
if let uploadable = uploadable {
|
||||
uploadTask = .upload(uploadable, nil)
|
||||
}
|
||||
|
||||
let underlyingError = error.underlyingAdaptError ?? error
|
||||
let upload = UploadRequest(session: session, requestTask: uploadTask, error: underlyingError)
|
||||
|
||||
if let retrier = retrier, error is AdaptError {
|
||||
allowRetrier(retrier, toRetry: upload, with: underlyingError)
|
||||
} else {
|
||||
if startRequestsImmediately { upload.resume() }
|
||||
}
|
||||
|
||||
return upload
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
// MARK: - Stream Request
|
||||
|
||||
// MARK: Hostname and Port
|
||||
|
||||
/// Creates a `StreamRequest` for bidirectional streaming using the `hostname` and `port`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter hostName: The hostname of the server to connect to.
|
||||
/// - parameter port: The port of the server to connect to.
|
||||
///
|
||||
/// - returns: The created `StreamRequest`.
|
||||
@discardableResult
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open func stream(withHostName hostName: String, port: Int) -> StreamRequest {
|
||||
return stream(.stream(hostName: hostName, port: port))
|
||||
}
|
||||
|
||||
// MARK: NetService
|
||||
|
||||
/// Creates a `StreamRequest` for bidirectional streaming using the `netService`.
|
||||
///
|
||||
/// If `startRequestsImmediately` is `true`, the request will have `resume()` called before being returned.
|
||||
///
|
||||
/// - parameter netService: The net service used to identify the endpoint.
|
||||
///
|
||||
/// - returns: The created `StreamRequest`.
|
||||
@discardableResult
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
open func stream(with netService: NetService) -> StreamRequest {
|
||||
return stream(.netService(netService))
|
||||
}
|
||||
|
||||
// MARK: Private - Stream Implementation
|
||||
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
private func stream(_ streamable: StreamRequest.Streamable) -> StreamRequest {
|
||||
do {
|
||||
let task = try streamable.task(session: session, adapter: adapter, queue: queue)
|
||||
let request = StreamRequest(session: session, requestTask: .stream(streamable, task))
|
||||
|
||||
delegate[task] = request
|
||||
|
||||
if startRequestsImmediately { request.resume() }
|
||||
|
||||
return request
|
||||
} catch {
|
||||
return stream(failedWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
|
||||
private func stream(failedWith error: Error) -> StreamRequest {
|
||||
let stream = StreamRequest(session: session, requestTask: .stream(nil, nil), error: error)
|
||||
if startRequestsImmediately { stream.resume() }
|
||||
return stream
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - Internal - Retry Request
|
||||
|
||||
func retry(_ request: Request) -> Bool {
|
||||
guard let originalTask = request.originalTask else { return false }
|
||||
|
||||
do {
|
||||
let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
|
||||
|
||||
if let originalTask = request.task {
|
||||
delegate[originalTask] = nil // removes the old request to avoid endless growth
|
||||
}
|
||||
|
||||
request.delegate.task = task // resets all task delegate data
|
||||
|
||||
request.retryCount += 1
|
||||
request.startTime = CFAbsoluteTimeGetCurrent()
|
||||
request.endTime = nil
|
||||
|
||||
task.resume()
|
||||
|
||||
return true
|
||||
} catch {
|
||||
request.delegate.error = error.underlyingAdaptError ?? error
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func allowRetrier(_ retrier: RequestRetrier, toRetry request: Request, with error: Error) {
|
||||
DispatchQueue.utility.async { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
retrier.should(strongSelf, retry: request, with: error) { shouldRetry, timeDelay in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
guard shouldRetry else {
|
||||
if strongSelf.startRequestsImmediately { request.resume() }
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.utility.after(timeDelay) {
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
let retrySucceeded = strongSelf.retry(request)
|
||||
|
||||
if retrySucceeded, let task = request.task {
|
||||
strongSelf.delegate[task] = request
|
||||
} else {
|
||||
if strongSelf.startRequestsImmediately { request.resume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
466
Pods/Alamofire/Source/TaskDelegate.swift
generated
Normal file
466
Pods/Alamofire/Source/TaskDelegate.swift
generated
Normal file
@ -0,0 +1,466 @@
|
||||
//
|
||||
// TaskDelegate.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The task delegate is responsible for handling all delegate callbacks for the underlying task as well as
|
||||
/// executing all operations attached to the serial operation queue upon task completion.
|
||||
open class TaskDelegate: NSObject {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The serial operation queue used to execute all operations after the task completes.
|
||||
public let queue: OperationQueue
|
||||
|
||||
/// The data returned by the server.
|
||||
public var data: Data? { return nil }
|
||||
|
||||
/// The error generated throughout the lifecyle of the task.
|
||||
public var error: Error?
|
||||
|
||||
var task: URLSessionTask? {
|
||||
set {
|
||||
taskLock.lock(); defer { taskLock.unlock() }
|
||||
_task = newValue
|
||||
}
|
||||
get {
|
||||
taskLock.lock(); defer { taskLock.unlock() }
|
||||
return _task
|
||||
}
|
||||
}
|
||||
|
||||
var initialResponseTime: CFAbsoluteTime?
|
||||
var credential: URLCredential?
|
||||
var metrics: AnyObject? // URLSessionTaskMetrics
|
||||
|
||||
private var _task: URLSessionTask? {
|
||||
didSet { reset() }
|
||||
}
|
||||
|
||||
private let taskLock = NSLock()
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(task: URLSessionTask?) {
|
||||
_task = task
|
||||
|
||||
self.queue = {
|
||||
let operationQueue = OperationQueue()
|
||||
|
||||
operationQueue.maxConcurrentOperationCount = 1
|
||||
operationQueue.isSuspended = true
|
||||
operationQueue.qualityOfService = .utility
|
||||
|
||||
return operationQueue
|
||||
}()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
error = nil
|
||||
initialResponseTime = nil
|
||||
}
|
||||
|
||||
// MARK: URLSessionTaskDelegate
|
||||
|
||||
var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
|
||||
var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
|
||||
var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
|
||||
var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?
|
||||
|
||||
@objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
willPerformHTTPRedirection response: HTTPURLResponse,
|
||||
newRequest request: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void)
|
||||
{
|
||||
var redirectRequest: URLRequest? = request
|
||||
|
||||
if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
|
||||
redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
|
||||
}
|
||||
|
||||
completionHandler(redirectRequest)
|
||||
}
|
||||
|
||||
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
|
||||
var credential: URLCredential?
|
||||
|
||||
if let taskDidReceiveChallenge = taskDidReceiveChallenge {
|
||||
(disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
|
||||
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
||||
let host = challenge.protectionSpace.host
|
||||
|
||||
if
|
||||
let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
|
||||
let serverTrust = challenge.protectionSpace.serverTrust
|
||||
{
|
||||
if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
|
||||
disposition = .useCredential
|
||||
credential = URLCredential(trust: serverTrust)
|
||||
} else {
|
||||
disposition = .cancelAuthenticationChallenge
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if challenge.previousFailureCount > 0 {
|
||||
disposition = .rejectProtectionSpace
|
||||
} else {
|
||||
credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)
|
||||
|
||||
if credential != nil {
|
||||
disposition = .useCredential
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(disposition, credential)
|
||||
}
|
||||
|
||||
@objc(URLSession:task:needNewBodyStream:)
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
|
||||
{
|
||||
var bodyStream: InputStream?
|
||||
|
||||
if let taskNeedNewBodyStream = taskNeedNewBodyStream {
|
||||
bodyStream = taskNeedNewBodyStream(session, task)
|
||||
}
|
||||
|
||||
completionHandler(bodyStream)
|
||||
}
|
||||
|
||||
@objc(URLSession:task:didCompleteWithError:)
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if let taskDidCompleteWithError = taskDidCompleteWithError {
|
||||
taskDidCompleteWithError(session, task, error)
|
||||
} else {
|
||||
if let error = error {
|
||||
if self.error == nil { self.error = error }
|
||||
|
||||
if
|
||||
let downloadDelegate = self as? DownloadTaskDelegate,
|
||||
let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
|
||||
{
|
||||
downloadDelegate.resumeData = resumeData
|
||||
}
|
||||
}
|
||||
|
||||
queue.isSuspended = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class DataTaskDelegate: TaskDelegate, URLSessionDataDelegate {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }
|
||||
|
||||
override var data: Data? {
|
||||
if dataStream != nil {
|
||||
return nil
|
||||
} else {
|
||||
return mutableData
|
||||
}
|
||||
}
|
||||
|
||||
var progress: Progress
|
||||
var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
|
||||
|
||||
var dataStream: ((_ data: Data) -> Void)?
|
||||
|
||||
private var totalBytesReceived: Int64 = 0
|
||||
private var mutableData: Data
|
||||
|
||||
private var expectedContentLength: Int64?
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
override init(task: URLSessionTask?) {
|
||||
mutableData = Data()
|
||||
progress = Progress(totalUnitCount: 0)
|
||||
|
||||
super.init(task: task)
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
progress = Progress(totalUnitCount: 0)
|
||||
totalBytesReceived = 0
|
||||
mutableData = Data()
|
||||
expectedContentLength = nil
|
||||
}
|
||||
|
||||
// MARK: URLSessionDataDelegate
|
||||
|
||||
var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
|
||||
var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
|
||||
var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
|
||||
var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didReceive response: URLResponse,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
|
||||
{
|
||||
var disposition: URLSession.ResponseDisposition = .allow
|
||||
|
||||
expectedContentLength = response.expectedContentLength
|
||||
|
||||
if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
|
||||
disposition = dataTaskDidReceiveResponse(session, dataTask, response)
|
||||
}
|
||||
|
||||
completionHandler(disposition)
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didBecome downloadTask: URLSessionDownloadTask)
|
||||
{
|
||||
dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
|
||||
|
||||
if let dataTaskDidReceiveData = dataTaskDidReceiveData {
|
||||
dataTaskDidReceiveData(session, dataTask, data)
|
||||
} else {
|
||||
if let dataStream = dataStream {
|
||||
dataStream(data)
|
||||
} else {
|
||||
mutableData.append(data)
|
||||
}
|
||||
|
||||
let bytesReceived = Int64(data.count)
|
||||
totalBytesReceived += bytesReceived
|
||||
let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
|
||||
|
||||
progress.totalUnitCount = totalBytesExpected
|
||||
progress.completedUnitCount = totalBytesReceived
|
||||
|
||||
if let progressHandler = progressHandler {
|
||||
progressHandler.queue.async { progressHandler.closure(self.progress) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
willCacheResponse proposedResponse: CachedURLResponse,
|
||||
completionHandler: @escaping (CachedURLResponse?) -> Void)
|
||||
{
|
||||
var cachedResponse: CachedURLResponse? = proposedResponse
|
||||
|
||||
if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
|
||||
cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
|
||||
}
|
||||
|
||||
completionHandler(cachedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class DownloadTaskDelegate: TaskDelegate, URLSessionDownloadDelegate {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }
|
||||
|
||||
var progress: Progress
|
||||
var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
|
||||
|
||||
var resumeData: Data?
|
||||
override var data: Data? { return resumeData }
|
||||
|
||||
var destination: DownloadRequest.DownloadFileDestination?
|
||||
|
||||
var temporaryURL: URL?
|
||||
var destinationURL: URL?
|
||||
|
||||
var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
override init(task: URLSessionTask?) {
|
||||
progress = Progress(totalUnitCount: 0)
|
||||
super.init(task: task)
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
progress = Progress(totalUnitCount: 0)
|
||||
resumeData = nil
|
||||
}
|
||||
|
||||
// MARK: URLSessionDownloadDelegate
|
||||
|
||||
var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
|
||||
var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
|
||||
var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didFinishDownloadingTo location: URL)
|
||||
{
|
||||
temporaryURL = location
|
||||
|
||||
guard
|
||||
let destination = destination,
|
||||
let response = downloadTask.response as? HTTPURLResponse
|
||||
else { return }
|
||||
|
||||
let result = destination(location, response)
|
||||
let destinationURL = result.destinationURL
|
||||
let options = result.options
|
||||
|
||||
self.destinationURL = destinationURL
|
||||
|
||||
do {
|
||||
if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
if options.contains(.createIntermediateDirectories) {
|
||||
let directory = destinationURL.deletingLastPathComponent()
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: location, to: destinationURL)
|
||||
} catch {
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64)
|
||||
{
|
||||
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
|
||||
|
||||
if let downloadTaskDidWriteData = downloadTaskDidWriteData {
|
||||
downloadTaskDidWriteData(
|
||||
session,
|
||||
downloadTask,
|
||||
bytesWritten,
|
||||
totalBytesWritten,
|
||||
totalBytesExpectedToWrite
|
||||
)
|
||||
} else {
|
||||
progress.totalUnitCount = totalBytesExpectedToWrite
|
||||
progress.completedUnitCount = totalBytesWritten
|
||||
|
||||
if let progressHandler = progressHandler {
|
||||
progressHandler.queue.async { progressHandler.closure(self.progress) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didResumeAtOffset fileOffset: Int64,
|
||||
expectedTotalBytes: Int64)
|
||||
{
|
||||
if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
|
||||
downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
|
||||
} else {
|
||||
progress.totalUnitCount = expectedTotalBytes
|
||||
progress.completedUnitCount = fileOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class UploadTaskDelegate: DataTaskDelegate {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }
|
||||
|
||||
var uploadProgress: Progress
|
||||
var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
override init(task: URLSessionTask?) {
|
||||
uploadProgress = Progress(totalUnitCount: 0)
|
||||
super.init(task: task)
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
uploadProgress = Progress(totalUnitCount: 0)
|
||||
}
|
||||
|
||||
// MARK: URLSessionTaskDelegate
|
||||
|
||||
var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
|
||||
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didSendBodyData bytesSent: Int64,
|
||||
totalBytesSent: Int64,
|
||||
totalBytesExpectedToSend: Int64)
|
||||
{
|
||||
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
|
||||
|
||||
if let taskDidSendBodyData = taskDidSendBodyData {
|
||||
taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
|
||||
} else {
|
||||
uploadProgress.totalUnitCount = totalBytesExpectedToSend
|
||||
uploadProgress.completedUnitCount = totalBytesSent
|
||||
|
||||
if let uploadProgressHandler = uploadProgressHandler {
|
||||
uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
Pods/Alamofire/Source/Timeline.swift
generated
Normal file
136
Pods/Alamofire/Source/Timeline.swift
generated
Normal file
@ -0,0 +1,136 @@
|
||||
//
|
||||
// Timeline.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Responsible for computing the timing metrics for the complete lifecycle of a `Request`.
|
||||
public struct Timeline {
|
||||
/// The time the request was initialized.
|
||||
public let requestStartTime: CFAbsoluteTime
|
||||
|
||||
/// The time the first bytes were received from or sent to the server.
|
||||
public let initialResponseTime: CFAbsoluteTime
|
||||
|
||||
/// The time when the request was completed.
|
||||
public let requestCompletedTime: CFAbsoluteTime
|
||||
|
||||
/// The time when the response serialization was completed.
|
||||
public let serializationCompletedTime: CFAbsoluteTime
|
||||
|
||||
/// The time interval in seconds from the time the request started to the initial response from the server.
|
||||
public let latency: TimeInterval
|
||||
|
||||
/// The time interval in seconds from the time the request started to the time the request completed.
|
||||
public let requestDuration: TimeInterval
|
||||
|
||||
/// The time interval in seconds from the time the request completed to the time response serialization completed.
|
||||
public let serializationDuration: TimeInterval
|
||||
|
||||
/// The time interval in seconds from the time the request started to the time response serialization completed.
|
||||
public let totalDuration: TimeInterval
|
||||
|
||||
/// Creates a new `Timeline` instance with the specified request times.
|
||||
///
|
||||
/// - parameter requestStartTime: The time the request was initialized. Defaults to `0.0`.
|
||||
/// - parameter initialResponseTime: The time the first bytes were received from or sent to the server.
|
||||
/// Defaults to `0.0`.
|
||||
/// - parameter requestCompletedTime: The time when the request was completed. Defaults to `0.0`.
|
||||
/// - parameter serializationCompletedTime: The time when the response serialization was completed. Defaults
|
||||
/// to `0.0`.
|
||||
///
|
||||
/// - returns: The new `Timeline` instance.
|
||||
public init(
|
||||
requestStartTime: CFAbsoluteTime = 0.0,
|
||||
initialResponseTime: CFAbsoluteTime = 0.0,
|
||||
requestCompletedTime: CFAbsoluteTime = 0.0,
|
||||
serializationCompletedTime: CFAbsoluteTime = 0.0)
|
||||
{
|
||||
self.requestStartTime = requestStartTime
|
||||
self.initialResponseTime = initialResponseTime
|
||||
self.requestCompletedTime = requestCompletedTime
|
||||
self.serializationCompletedTime = serializationCompletedTime
|
||||
|
||||
self.latency = initialResponseTime - requestStartTime
|
||||
self.requestDuration = requestCompletedTime - requestStartTime
|
||||
self.serializationDuration = serializationCompletedTime - requestCompletedTime
|
||||
self.totalDuration = serializationCompletedTime - requestStartTime
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Timeline: CustomStringConvertible {
|
||||
/// The textual representation used when written to an output stream, which includes the latency, the request
|
||||
/// duration and the total duration.
|
||||
public var description: String {
|
||||
let latency = String(format: "%.3f", self.latency)
|
||||
let requestDuration = String(format: "%.3f", self.requestDuration)
|
||||
let serializationDuration = String(format: "%.3f", self.serializationDuration)
|
||||
let totalDuration = String(format: "%.3f", self.totalDuration)
|
||||
|
||||
// NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is
|
||||
// fixed, we should move back to string interpolation by reverting commit 7d4a43b1.
|
||||
let timings = [
|
||||
"\"Latency\": " + latency + " secs",
|
||||
"\"Request Duration\": " + requestDuration + " secs",
|
||||
"\"Serialization Duration\": " + serializationDuration + " secs",
|
||||
"\"Total Duration\": " + totalDuration + " secs"
|
||||
]
|
||||
|
||||
return "Timeline: { " + timings.joined(separator: ", ") + " }"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomDebugStringConvertible
|
||||
|
||||
extension Timeline: CustomDebugStringConvertible {
|
||||
/// The textual representation used when written to an output stream, which includes the request start time, the
|
||||
/// initial response time, the request completed time, the serialization completed time, the latency, the request
|
||||
/// duration and the total duration.
|
||||
public var debugDescription: String {
|
||||
let requestStartTime = String(format: "%.3f", self.requestStartTime)
|
||||
let initialResponseTime = String(format: "%.3f", self.initialResponseTime)
|
||||
let requestCompletedTime = String(format: "%.3f", self.requestCompletedTime)
|
||||
let serializationCompletedTime = String(format: "%.3f", self.serializationCompletedTime)
|
||||
let latency = String(format: "%.3f", self.latency)
|
||||
let requestDuration = String(format: "%.3f", self.requestDuration)
|
||||
let serializationDuration = String(format: "%.3f", self.serializationDuration)
|
||||
let totalDuration = String(format: "%.3f", self.totalDuration)
|
||||
|
||||
// NOTE: Had to move to string concatenation due to memory leak filed as rdar://26761490. Once memory leak is
|
||||
// fixed, we should move back to string interpolation by reverting commit 7d4a43b1.
|
||||
let timings = [
|
||||
"\"Request Start Time\": " + requestStartTime,
|
||||
"\"Initial Response Time\": " + initialResponseTime,
|
||||
"\"Request Completed Time\": " + requestCompletedTime,
|
||||
"\"Serialization Completed Time\": " + serializationCompletedTime,
|
||||
"\"Latency\": " + latency + " secs",
|
||||
"\"Request Duration\": " + requestDuration + " secs",
|
||||
"\"Serialization Duration\": " + serializationDuration + " secs",
|
||||
"\"Total Duration\": " + totalDuration + " secs"
|
||||
]
|
||||
|
||||
return "Timeline: { " + timings.joined(separator: ", ") + " }"
|
||||
}
|
||||
}
|
||||
315
Pods/Alamofire/Source/Validation.swift
generated
Normal file
315
Pods/Alamofire/Source/Validation.swift
generated
Normal file
@ -0,0 +1,315 @@
|
||||
//
|
||||
// Validation.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Request {
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason
|
||||
|
||||
/// Used to represent whether validation was successful or encountered an error resulting in a failure.
|
||||
///
|
||||
/// - success: The validation was successful.
|
||||
/// - failure: The validation failed encountering the provided error.
|
||||
public enum ValidationResult {
|
||||
case success
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
fileprivate struct MIMEType {
|
||||
let type: String
|
||||
let subtype: String
|
||||
|
||||
var isWildcard: Bool { return type == "*" && subtype == "*" }
|
||||
|
||||
init?(_ string: String) {
|
||||
let components: [String] = {
|
||||
let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
#if swift(>=3.2)
|
||||
let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)]
|
||||
#else
|
||||
let split = stripped.substring(to: stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)
|
||||
#endif
|
||||
|
||||
return split.components(separatedBy: "/")
|
||||
}()
|
||||
|
||||
if let type = components.first, let subtype = components.last {
|
||||
self.type = type
|
||||
self.subtype = subtype
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func matches(_ mime: MIMEType) -> Bool {
|
||||
switch (type, subtype) {
|
||||
case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
fileprivate var acceptableStatusCodes: [Int] { return Array(200..<300) }
|
||||
|
||||
fileprivate var acceptableContentTypes: [String] {
|
||||
if let accept = request?.value(forHTTPHeaderField: "Accept") {
|
||||
return accept.components(separatedBy: ",")
|
||||
}
|
||||
|
||||
return ["*/*"]
|
||||
}
|
||||
|
||||
// MARK: Status Code
|
||||
|
||||
fileprivate func validate<S: Sequence>(
|
||||
statusCode acceptableStatusCodes: S,
|
||||
response: HTTPURLResponse)
|
||||
-> ValidationResult
|
||||
where S.Iterator.Element == Int
|
||||
{
|
||||
if acceptableStatusCodes.contains(response.statusCode) {
|
||||
return .success
|
||||
} else {
|
||||
let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode)
|
||||
return .failure(AFError.responseValidationFailed(reason: reason))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Content Type
|
||||
|
||||
fileprivate func validate<S: Sequence>(
|
||||
contentType acceptableContentTypes: S,
|
||||
response: HTTPURLResponse,
|
||||
data: Data?)
|
||||
-> ValidationResult
|
||||
where S.Iterator.Element == String
|
||||
{
|
||||
guard let data = data, data.count > 0 else { return .success }
|
||||
|
||||
guard
|
||||
let responseContentType = response.mimeType,
|
||||
let responseMIMEType = MIMEType(responseContentType)
|
||||
else {
|
||||
for contentType in acceptableContentTypes {
|
||||
if let mimeType = MIMEType(contentType), mimeType.isWildcard {
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
let error: AFError = {
|
||||
let reason: ErrorReason = .missingContentType(acceptableContentTypes: Array(acceptableContentTypes))
|
||||
return AFError.responseValidationFailed(reason: reason)
|
||||
}()
|
||||
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
for contentType in acceptableContentTypes {
|
||||
if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) {
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
let error: AFError = {
|
||||
let reason: ErrorReason = .unacceptableContentType(
|
||||
acceptableContentTypes: Array(acceptableContentTypes),
|
||||
responseContentType: responseContentType
|
||||
)
|
||||
|
||||
return AFError.responseValidationFailed(reason: reason)
|
||||
}()
|
||||
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension DataRequest {
|
||||
/// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the
|
||||
/// request was valid.
|
||||
public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
|
||||
|
||||
/// Validates the request, using the specified closure.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - parameter validation: A closure to validate the request.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate(_ validation: @escaping Validation) -> Self {
|
||||
let validationExecution: () -> Void = { [unowned self] in
|
||||
if
|
||||
let response = self.response,
|
||||
self.delegate.error == nil,
|
||||
case let .failure(error) = validation(self.request, response, self.delegate.data)
|
||||
{
|
||||
self.delegate.error = error
|
||||
}
|
||||
}
|
||||
|
||||
validations.append(validationExecution)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Validates that the response has a status code in the specified sequence.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - parameter range: The range of acceptable status codes.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
|
||||
return validate { [unowned self] _, response, _ in
|
||||
return self.validate(statusCode: acceptableStatusCodes, response: response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that the response has a content type in the specified sequence.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate<S: Sequence>(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String {
|
||||
return validate { [unowned self] _, response, data in
|
||||
return self.validate(contentType: acceptableContentTypes, response: response, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
|
||||
/// type matches any specified in the Accept HTTP header field.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate() -> Self {
|
||||
return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension DownloadRequest {
|
||||
/// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a
|
||||
/// destination URL, and returns whether the request was valid.
|
||||
public typealias Validation = (
|
||||
_ request: URLRequest?,
|
||||
_ response: HTTPURLResponse,
|
||||
_ temporaryURL: URL?,
|
||||
_ destinationURL: URL?)
|
||||
-> ValidationResult
|
||||
|
||||
/// Validates the request, using the specified closure.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - parameter validation: A closure to validate the request.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate(_ validation: @escaping Validation) -> Self {
|
||||
let validationExecution: () -> Void = { [unowned self] in
|
||||
let request = self.request
|
||||
let temporaryURL = self.downloadDelegate.temporaryURL
|
||||
let destinationURL = self.downloadDelegate.destinationURL
|
||||
|
||||
if
|
||||
let response = self.response,
|
||||
self.delegate.error == nil,
|
||||
case let .failure(error) = validation(request, response, temporaryURL, destinationURL)
|
||||
{
|
||||
self.delegate.error = error
|
||||
}
|
||||
}
|
||||
|
||||
validations.append(validationExecution)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// Validates that the response has a status code in the specified sequence.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - parameter range: The range of acceptable status codes.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
|
||||
return validate { [unowned self] _, response, _, _ in
|
||||
return self.validate(statusCode: acceptableStatusCodes, response: response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that the response has a content type in the specified sequence.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate<S: Sequence>(contentType acceptableContentTypes: S) -> Self where S.Iterator.Element == String {
|
||||
return validate { [unowned self] _, response, _, _ in
|
||||
let fileURL = self.downloadDelegate.fileURL
|
||||
|
||||
guard let validFileURL = fileURL else {
|
||||
return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: validFileURL)
|
||||
return self.validate(contentType: acceptableContentTypes, response: response, data: data)
|
||||
} catch {
|
||||
return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that the response has a status code in the default acceptable range of 200...299, and that the content
|
||||
/// type matches any specified in the Accept HTTP header field.
|
||||
///
|
||||
/// If validation fails, subsequent calls to response handlers will have an associated error.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func validate() -> Self {
|
||||
return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes)
|
||||
}
|
||||
}
|
||||
202
Pods/GTMSessionFetcher/LICENSE
generated
Normal file
202
Pods/GTMSessionFetcher/LICENSE
generated
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
23
Pods/GTMSessionFetcher/README.md
generated
Normal file
23
Pods/GTMSessionFetcher/README.md
generated
Normal file
@ -0,0 +1,23 @@
|
||||
# Google Toolbox for Mac - Session Fetcher #
|
||||
|
||||
**Project site** <https://github.com/google/gtm-session-fetcher><br>
|
||||
**Discussion group** <http://groups.google.com/group/google-toolbox-for-mac>
|
||||
|
||||
[](https://travis-ci.org/google/gtm-session-fetcher)
|
||||
|
||||
`GTMSessionFetcher` makes it easy for Cocoa applications to perform http
|
||||
operations. The fetcher is implemented as a wrapper on `NSURLSession`, so its
|
||||
behavior is asynchronous and uses operating-system settings on iOS and Mac OS X.
|
||||
|
||||
Features include:
|
||||
- Simple to build; only one source/header file pair is required
|
||||
- Simple to use: takes just two lines of code to fetch a request
|
||||
- Supports upload and download sessions
|
||||
- Flexible cookie storage
|
||||
- Automatic retry on errors, with exponential backoff
|
||||
- Support for generating multipart MIME upload streams
|
||||
- Easy, convenient logging of http requests and responses
|
||||
- Supports plug-in authentication such as with GTMAppAuth
|
||||
- Easily testable; self-mocking
|
||||
- Automatic rate limiting when created by the `GTMSessionFetcherService` factory class
|
||||
- Fully independent of other projects
|
||||
52
Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h
generated
Normal file
52
Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h
generated
Normal file
@ -0,0 +1,52 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// The GTMGatherInput stream is an input stream implementation that is to be
|
||||
// instantiated with an NSArray of NSData objects. It works in the traditional
|
||||
// scatter/gather vector I/O model. Rather than allocating a big NSData object
|
||||
// to hold all of the data and performing a copy into that object, the
|
||||
// GTMGatherInputStream will maintain a reference to the NSArray and read from
|
||||
// each NSData in turn as the read method is called. You should not alter the
|
||||
// underlying set of NSData objects until all read operations on this input
|
||||
// stream have completed.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifndef GTM_NONNULL
|
||||
#if defined(__has_attribute)
|
||||
#if __has_attribute(nonnull)
|
||||
#define GTM_NONNULL(x) __attribute__((nonnull x))
|
||||
#else
|
||||
#define GTM_NONNULL(x)
|
||||
#endif
|
||||
#else
|
||||
#define GTM_NONNULL(x)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Avoid multiple declaration of this class.
|
||||
//
|
||||
// Note: This should match the declaration of GTMGatherInputStream in GTMMIMEDocument.m
|
||||
|
||||
#ifndef GTM_GATHERINPUTSTREAM_DECLARED
|
||||
#define GTM_GATHERINPUTSTREAM_DECLARED
|
||||
|
||||
@interface GTMGatherInputStream : NSInputStream <NSStreamDelegate>
|
||||
|
||||
+ (NSInputStream *)streamWithArray:(NSArray *)dataArray GTM_NONNULL((1));
|
||||
|
||||
@end
|
||||
|
||||
#endif // GTM_GATHERINPUTSTREAM_DECLARED
|
||||
185
Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m
generated
Normal file
185
Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m
generated
Normal file
@ -0,0 +1,185 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if !defined(__has_feature) || !__has_feature(objc_arc)
|
||||
#error "This file requires ARC support."
|
||||
#endif
|
||||
|
||||
#import "GTMGatherInputStream.h"
|
||||
|
||||
@implementation GTMGatherInputStream {
|
||||
NSArray *_dataArray; // NSDatas that should be "gathered" and streamed.
|
||||
NSUInteger _arrayIndex; // Index in the array of the current NSData.
|
||||
long long _dataOffset; // Offset in the current NSData we are processing.
|
||||
NSStreamStatus _streamStatus;
|
||||
id<NSStreamDelegate> __weak _delegate; // Stream delegate, defaults to self.
|
||||
}
|
||||
|
||||
+ (NSInputStream *)streamWithArray:(NSArray *)dataArray {
|
||||
return [(GTMGatherInputStream *)[self alloc] initWithArray:dataArray];
|
||||
}
|
||||
|
||||
- (instancetype)initWithArray:(NSArray *)dataArray {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_dataArray = dataArray;
|
||||
_delegate = self; // An NSStream's default delegate should be self.
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - NSStream
|
||||
|
||||
- (void)open {
|
||||
_arrayIndex = 0;
|
||||
_dataOffset = 0;
|
||||
_streamStatus = NSStreamStatusOpen;
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
_streamStatus = NSStreamStatusClosed;
|
||||
}
|
||||
|
||||
- (id<NSStreamDelegate>)delegate {
|
||||
return _delegate;
|
||||
}
|
||||
|
||||
- (void)setDelegate:(id<NSStreamDelegate>)delegate {
|
||||
if (delegate == nil) {
|
||||
_delegate = self;
|
||||
} else {
|
||||
_delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)propertyForKey:(NSString *)key {
|
||||
if ([key isEqual:NSStreamFileCurrentOffsetKey]) {
|
||||
return @([self absoluteOffset]);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)setProperty:(id)property forKey:(NSString *)key {
|
||||
if ([key isEqual:NSStreamFileCurrentOffsetKey]) {
|
||||
NSNumber *absoluteOffsetNumber = property;
|
||||
[self setAbsoluteOffset:absoluteOffsetNumber.longLongValue];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
|
||||
}
|
||||
|
||||
- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
|
||||
}
|
||||
|
||||
- (NSStreamStatus)streamStatus {
|
||||
return _streamStatus;
|
||||
}
|
||||
|
||||
- (NSError *)streamError {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - NSInputStream
|
||||
|
||||
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
|
||||
NSInteger bytesRead = 0;
|
||||
NSUInteger bytesRemaining = len;
|
||||
|
||||
// Read bytes from the currently-indexed array.
|
||||
while ((bytesRemaining > 0) && (_arrayIndex < _dataArray.count)) {
|
||||
NSData *data = [_dataArray objectAtIndex:_arrayIndex];
|
||||
|
||||
NSUInteger dataLen = data.length;
|
||||
NSUInteger dataBytesLeft = dataLen - (NSUInteger)_dataOffset;
|
||||
|
||||
NSUInteger bytesToCopy = MIN(bytesRemaining, dataBytesLeft);
|
||||
NSRange range = NSMakeRange((NSUInteger) _dataOffset, bytesToCopy);
|
||||
|
||||
[data getBytes:(buffer + bytesRead) range:range];
|
||||
|
||||
bytesRead += bytesToCopy;
|
||||
_dataOffset += bytesToCopy;
|
||||
bytesRemaining -= bytesToCopy;
|
||||
|
||||
if (_dataOffset == (long long)dataLen) {
|
||||
_dataOffset = 0;
|
||||
_arrayIndex++;
|
||||
}
|
||||
}
|
||||
if (_arrayIndex >= _dataArray.count) {
|
||||
_streamStatus = NSStreamStatusAtEnd;
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len {
|
||||
return NO; // We don't support this style of reading.
|
||||
}
|
||||
|
||||
- (BOOL)hasBytesAvailable {
|
||||
// If we return no, the read never finishes, even if we've already delivered all the bytes.
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - NSStreamDelegate
|
||||
|
||||
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
|
||||
id<NSStreamDelegate> delegate = _delegate;
|
||||
if (delegate != self) {
|
||||
[delegate stream:self handleEvent:streamEvent];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (long long)absoluteOffset {
|
||||
long long absoluteOffset = 0;
|
||||
NSUInteger index = 0;
|
||||
for (NSData *data in _dataArray) {
|
||||
if (index >= _arrayIndex) {
|
||||
break;
|
||||
}
|
||||
absoluteOffset += data.length;
|
||||
++index;
|
||||
}
|
||||
absoluteOffset += _dataOffset;
|
||||
return absoluteOffset;
|
||||
}
|
||||
|
||||
- (void)setAbsoluteOffset:(long long)absoluteOffset {
|
||||
if (absoluteOffset < 0) {
|
||||
absoluteOffset = 0;
|
||||
}
|
||||
_arrayIndex = 0;
|
||||
_dataOffset = absoluteOffset;
|
||||
for (NSData *data in _dataArray) {
|
||||
long long dataLen = (long long) data.length;
|
||||
if (dataLen > _dataOffset) {
|
||||
break;
|
||||
}
|
||||
_arrayIndex++;
|
||||
_dataOffset -= dataLen;
|
||||
}
|
||||
if (_arrayIndex == _dataArray.count) {
|
||||
if (_dataOffset > 0) {
|
||||
_dataOffset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
148
Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h
generated
Normal file
148
Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h
generated
Normal file
@ -0,0 +1,148 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This is a simple class to create or parse a MIME document.
|
||||
|
||||
// To create a MIME document, allocate a new GTMMIMEDocument and start adding parts.
|
||||
// When you are done adding parts, call generateInputStream or generateDispatchData.
|
||||
//
|
||||
// A good reference for MIME is http://en.wikipedia.org/wiki/MIME
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifndef GTM_NONNULL
|
||||
#if defined(__has_attribute)
|
||||
#if __has_attribute(nonnull)
|
||||
#define GTM_NONNULL(x) __attribute__((nonnull x))
|
||||
#else
|
||||
#define GTM_NONNULL(x)
|
||||
#endif
|
||||
#else
|
||||
#define GTM_NONNULL(x)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef GTM_DECLARE_GENERICS
|
||||
#if __has_feature(objc_generics)
|
||||
#define GTM_DECLARE_GENERICS 1
|
||||
#else
|
||||
#define GTM_DECLARE_GENERICS 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef GTM_NSArrayOf
|
||||
#if GTM_DECLARE_GENERICS
|
||||
#define GTM_NSArrayOf(value) NSArray<value>
|
||||
#define GTM_NSDictionaryOf(key, value) NSDictionary<key, value>
|
||||
#else
|
||||
#define GTM_NSArrayOf(value) NSArray
|
||||
#define GTM_NSDictionaryOf(key, value) NSDictionary
|
||||
#endif // GTM_DECLARE_GENERICS
|
||||
#endif // GTM_NSArrayOf
|
||||
|
||||
|
||||
// GTMMIMEDocumentPart represents a part of a MIME document.
|
||||
//
|
||||
// +[GTMMIMEDocument MIMEPartsWithBoundary:data:] returns an array of these.
|
||||
@interface GTMMIMEDocumentPart : NSObject
|
||||
|
||||
@property(nonatomic, readonly) GTM_NSDictionaryOf(NSString *, NSString *) *headers;
|
||||
@property(nonatomic, readonly) NSData *headerData;
|
||||
@property(nonatomic, readonly) NSData *body;
|
||||
@property(nonatomic, readonly) NSUInteger length;
|
||||
|
||||
+ (instancetype)partWithHeaders:(NSDictionary *)headers body:(NSData *)body;
|
||||
|
||||
@end
|
||||
|
||||
@interface GTMMIMEDocument : NSObject
|
||||
|
||||
// Get or set the unique boundary for the parts that have been added.
|
||||
//
|
||||
// When creating a MIME document from parts, this is typically calculated
|
||||
// automatically after all parts have been added.
|
||||
@property(nonatomic, copy) NSString *boundary;
|
||||
|
||||
#pragma mark - Methods for Creating a MIME Document
|
||||
|
||||
+ (instancetype)MIMEDocument;
|
||||
|
||||
// Adds a new part to this mime document with the given headers and body.
|
||||
// The headers keys and values should be NSStrings.
|
||||
// Adding a part may cause the boundary string to change.
|
||||
- (void)addPartWithHeaders:(GTM_NSDictionaryOf(NSString *, NSString *) *)headers
|
||||
body:(NSData *)body GTM_NONNULL((1,2));
|
||||
|
||||
// An inputstream that can be used to efficiently read the contents of the MIME document.
|
||||
//
|
||||
// Any parameter may be null if the result is not wanted.
|
||||
- (void)generateInputStream:(NSInputStream **)outStream
|
||||
length:(unsigned long long *)outLength
|
||||
boundary:(NSString **)outBoundary;
|
||||
|
||||
// A dispatch_data_t with the contents of the MIME document.
|
||||
//
|
||||
// Note: dispatch_data_t is one-way toll-free bridged so the result
|
||||
// may be cast directly to NSData *.
|
||||
//
|
||||
// Any parameter may be null if the result is not wanted.
|
||||
- (void)generateDispatchData:(dispatch_data_t *)outDispatchData
|
||||
length:(unsigned long long *)outLength
|
||||
boundary:(NSString **)outBoundary;
|
||||
|
||||
// Utility method for making a header section, including trailing newlines.
|
||||
+ (NSData *)dataWithHeaders:(GTM_NSDictionaryOf(NSString *, NSString *) *)headers;
|
||||
|
||||
#pragma mark - Methods for Parsing a MIME Document
|
||||
|
||||
// Method for parsing out an array of MIME parts from a MIME document.
|
||||
//
|
||||
// Returns an array of GTMMIMEDocumentParts. Returns nil if no part can
|
||||
// be found.
|
||||
+ (GTM_NSArrayOf(GTMMIMEDocumentPart *) *)MIMEPartsWithBoundary:(NSString *)boundary
|
||||
data:(NSData *)fullDocumentData;
|
||||
|
||||
// Utility method for efficiently searching possibly discontiguous NSData
|
||||
// for occurrences of target byte. This method does not "flatten" an NSData
|
||||
// that is composed of discontiguous blocks.
|
||||
//
|
||||
// The byte offsets of non-overlapping occurrences of the target are returned as
|
||||
// NSNumbers in the array.
|
||||
+ (void)searchData:(NSData *)data
|
||||
targetBytes:(const void *)targetBytes
|
||||
targetLength:(NSUInteger)targetLength
|
||||
foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets;
|
||||
|
||||
// Utility method to parse header bytes into an NSDictionary.
|
||||
+ (GTM_NSDictionaryOf(NSString *, NSString *) *)headersWithData:(NSData *)data;
|
||||
|
||||
// ------ UNIT TESTING ONLY BELOW ------
|
||||
|
||||
// Internal methods, exposed for unit testing only.
|
||||
- (void)seedRandomWith:(u_int32_t)seed;
|
||||
|
||||
+ (NSUInteger)findBytesWithNeedle:(const unsigned char *)needle
|
||||
needleLength:(NSUInteger)needleLength
|
||||
haystack:(const unsigned char *)haystack
|
||||
haystackLength:(NSUInteger)haystackLength
|
||||
foundOffset:(NSUInteger *)foundOffset;
|
||||
|
||||
+ (void)searchData:(NSData *)data
|
||||
targetBytes:(const void *)targetBytes
|
||||
targetLength:(NSUInteger)targetLength
|
||||
foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets
|
||||
foundBlockNumbers:(GTM_NSArrayOf(NSNumber *) **)outFoundBlockNumbers;
|
||||
|
||||
@end
|
||||
631
Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m
generated
Normal file
631
Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m
generated
Normal file
@ -0,0 +1,631 @@
|
||||
/* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if !defined(__has_feature) || !__has_feature(objc_arc)
|
||||
#error "This file requires ARC support."
|
||||
#endif
|
||||
|
||||
#import "GTMMIMEDocument.h"
|
||||
|
||||
// Avoid a hard dependency on GTMGatherInputStream.
|
||||
#ifndef GTM_GATHERINPUTSTREAM_DECLARED
|
||||
#define GTM_GATHERINPUTSTREAM_DECLARED
|
||||
|
||||
@interface GTMGatherInputStream : NSInputStream <NSStreamDelegate>
|
||||
|
||||
+ (NSInputStream *)streamWithArray:(NSArray *)dataArray GTM_NONNULL((1));
|
||||
|
||||
@end
|
||||
#endif // GTM_GATHERINPUTSTREAM_DECLARED
|
||||
|
||||
// FindBytes
|
||||
//
|
||||
// Helper routine to search for the existence of a set of bytes (needle) within
|
||||
// a presumed larger set of bytes (haystack). Can find the first part of the
|
||||
// needle at the very end of the haystack.
|
||||
//
|
||||
// Returns the needle length on complete success, the number of bytes matched
|
||||
// if a partial needle was found at the end of the haystack, and 0 on failure.
|
||||
static NSUInteger FindBytes(const unsigned char *needle, NSUInteger needleLen,
|
||||
const unsigned char *haystack, NSUInteger haystackLen,
|
||||
NSUInteger *foundOffset);
|
||||
|
||||
// SearchDataForBytes
|
||||
//
|
||||
// This implements the functionality of the +searchData: methods below. See the documentation
|
||||
// for those methods.
|
||||
static void SearchDataForBytes(NSData *data, const void *targetBytes, NSUInteger targetLength,
|
||||
NSMutableArray *foundOffsets, NSMutableArray *foundBlockNumbers);
|
||||
|
||||
@implementation GTMMIMEDocumentPart {
|
||||
NSDictionary *_headers;
|
||||
NSData *_headerData; // Header content including the ending "\r\n".
|
||||
NSData *_bodyData;
|
||||
}
|
||||
|
||||
@synthesize headers = _headers,
|
||||
headerData = _headerData,
|
||||
body = _bodyData;
|
||||
|
||||
@dynamic length;
|
||||
|
||||
+ (instancetype)partWithHeaders:(NSDictionary *)headers body:(NSData *)body {
|
||||
return [[self alloc] initWithHeaders:headers body:body];
|
||||
}
|
||||
|
||||
- (instancetype)initWithHeaders:(NSDictionary *)headers body:(NSData *)body {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_bodyData = body;
|
||||
_headers = headers;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// Returns true if the part's header or data contain the given set of bytes.
|
||||
//
|
||||
// NOTE: We assume that the 'bytes' we are checking for do not contain "\r\n",
|
||||
// so we don't need to check the concatenation of the header and body bytes.
|
||||
- (BOOL)containsBytes:(const unsigned char *)bytes length:(NSUInteger)length {
|
||||
// This uses custom search code rather than strcpy because the encoded data may contain
|
||||
// null values.
|
||||
NSData *headerData = self.headerData;
|
||||
return (FindBytes(bytes, length, headerData.bytes, headerData.length, NULL) == length ||
|
||||
FindBytes(bytes, length, _bodyData.bytes, _bodyData.length, NULL) == length);
|
||||
}
|
||||
|
||||
- (NSData *)headerData {
|
||||
if (!_headerData) {
|
||||
_headerData = [GTMMIMEDocument dataWithHeaders:_headers];
|
||||
}
|
||||
return _headerData;
|
||||
}
|
||||
|
||||
- (NSData *)body {
|
||||
return _bodyData;
|
||||
}
|
||||
|
||||
- (NSUInteger)length {
|
||||
return _headerData.length + _bodyData.length;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"%@ %p (headers %lu keys, body %lu bytes)",
|
||||
[self class], self, (unsigned long)_headers.count,
|
||||
(unsigned long)_bodyData.length];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(GTMMIMEDocumentPart *)other {
|
||||
if (self == other) return YES;
|
||||
if (![other isKindOfClass:[GTMMIMEDocumentPart class]]) return NO;
|
||||
return ((_bodyData == other->_bodyData || [_bodyData isEqual:other->_bodyData])
|
||||
&& (_headers == other->_headers || [_headers isEqual:other->_headers]));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return _bodyData.hash | _headers.hash;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation GTMMIMEDocument {
|
||||
NSMutableArray *_parts; // Ordered array of GTMMIMEDocumentParts.
|
||||
unsigned long long _length; // Length in bytes of the document.
|
||||
NSString *_boundary;
|
||||
u_int32_t _randomSeed; // For testing.
|
||||
}
|
||||
|
||||
+ (instancetype)MIMEDocument {
|
||||
return [[self alloc] init];
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_parts = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"%@ %p (%lu parts)",
|
||||
[self class], self, (unsigned long)_parts.count];
|
||||
}
|
||||
|
||||
#pragma mark - Joining Parts
|
||||
|
||||
// Adds a new part to this mime document with the given headers and body.
|
||||
- (void)addPartWithHeaders:(NSDictionary *)headers body:(NSData *)body {
|
||||
GTMMIMEDocumentPart *part = [GTMMIMEDocumentPart partWithHeaders:headers body:body];
|
||||
[_parts addObject:part];
|
||||
_boundary = nil;
|
||||
}
|
||||
|
||||
// For unit testing only, seeds the random number generator so that we will
|
||||
// have reproducible boundary strings.
|
||||
- (void)seedRandomWith:(u_int32_t)seed {
|
||||
_randomSeed = seed;
|
||||
_boundary = nil;
|
||||
}
|
||||
|
||||
- (u_int32_t)random {
|
||||
if (_randomSeed) {
|
||||
// For testing only.
|
||||
return _randomSeed++;
|
||||
} else {
|
||||
return arc4random();
|
||||
}
|
||||
}
|
||||
|
||||
// Computes the mime boundary to use. This should only be called
|
||||
// after all the desired document parts have been added since it must compute
|
||||
// a boundary that does not exist in the document data.
|
||||
- (NSString *)boundary {
|
||||
if (_boundary) {
|
||||
return _boundary;
|
||||
}
|
||||
|
||||
// Use an easily-readable boundary string.
|
||||
NSString *const kBaseBoundary = @"END_OF_PART";
|
||||
|
||||
_boundary = kBaseBoundary;
|
||||
|
||||
// If the boundary isn't unique, append random numbers, up to 10 attempts;
|
||||
// if that's still not unique, use a random number sequence instead, and call it good.
|
||||
BOOL didCollide = NO;
|
||||
|
||||
const int maxTries = 10; // Arbitrarily chosen maximum attempts.
|
||||
for (int tries = 0; tries < maxTries; ++tries) {
|
||||
|
||||
NSData *data = [_boundary dataUsingEncoding:NSUTF8StringEncoding];
|
||||
const void *dataBytes = data.bytes;
|
||||
NSUInteger dataLen = data.length;
|
||||
|
||||
for (GTMMIMEDocumentPart *part in _parts) {
|
||||
didCollide = [part containsBytes:dataBytes length:dataLen];
|
||||
if (didCollide) break;
|
||||
}
|
||||
|
||||
if (!didCollide) break; // We're fine, no more attempts needed.
|
||||
|
||||
// Try again with a random number appended.
|
||||
_boundary = [NSString stringWithFormat:@"%@_%08x", kBaseBoundary, [self random]];
|
||||
}
|
||||
|
||||
if (didCollide) {
|
||||
// Fallback... two random numbers.
|
||||
_boundary = [NSString stringWithFormat:@"%08x_tedborg_%08x", [self random], [self random]];
|
||||
}
|
||||
return _boundary;
|
||||
}
|
||||
|
||||
- (void)setBoundary:(NSString *)str {
|
||||
_boundary = [str copy];
|
||||
}
|
||||
|
||||
// Internal method.
|
||||
- (void)generateDataArray:(NSMutableArray *)dataArray
|
||||
length:(unsigned long long *)outLength
|
||||
boundary:(NSString **)outBoundary {
|
||||
|
||||
// The input stream is of the form:
|
||||
// --boundary
|
||||
// [part_1_headers]
|
||||
// [part_1_data]
|
||||
// --boundary
|
||||
// [part_2_headers]
|
||||
// [part_2_data]
|
||||
// --boundary--
|
||||
|
||||
// First we set up our boundary NSData objects.
|
||||
NSString *boundary = self.boundary;
|
||||
|
||||
NSString *mainBoundary = [NSString stringWithFormat:@"\r\n--%@\r\n", boundary];
|
||||
NSString *endBoundary = [NSString stringWithFormat:@"\r\n--%@--\r\n", boundary];
|
||||
|
||||
NSData *mainBoundaryData = [mainBoundary dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *endBoundaryData = [endBoundary dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
// Now we add them all in proper order to our dataArray.
|
||||
unsigned long long length = 0;
|
||||
|
||||
for (GTMMIMEDocumentPart *part in _parts) {
|
||||
[dataArray addObject:mainBoundaryData];
|
||||
[dataArray addObject:part.headerData];
|
||||
[dataArray addObject:part.body];
|
||||
|
||||
length += part.length + mainBoundaryData.length;
|
||||
}
|
||||
|
||||
[dataArray addObject:endBoundaryData];
|
||||
length += endBoundaryData.length;
|
||||
|
||||
if (outLength) *outLength = length;
|
||||
if (outBoundary) *outBoundary = boundary;
|
||||
}
|
||||
|
||||
- (void)generateInputStream:(NSInputStream **)outStream
|
||||
length:(unsigned long long *)outLength
|
||||
boundary:(NSString **)outBoundary {
|
||||
NSMutableArray *dataArray = outStream ? [NSMutableArray array] : nil;
|
||||
[self generateDataArray:dataArray
|
||||
length:outLength
|
||||
boundary:outBoundary];
|
||||
|
||||
if (outStream) {
|
||||
Class streamClass = NSClassFromString(@"GTMGatherInputStream");
|
||||
NSAssert(streamClass != nil, @"GTMGatherInputStream not available.");
|
||||
|
||||
*outStream = [streamClass streamWithArray:dataArray];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)generateDispatchData:(dispatch_data_t *)outDispatchData
|
||||
length:(unsigned long long *)outLength
|
||||
boundary:(NSString **)outBoundary {
|
||||
NSMutableArray *dataArray = outDispatchData ? [NSMutableArray array] : nil;
|
||||
[self generateDataArray:dataArray
|
||||
length:outLength
|
||||
boundary:outBoundary];
|
||||
|
||||
if (outDispatchData) {
|
||||
// Create an empty data accumulator.
|
||||
dispatch_data_t dataAccumulator;
|
||||
|
||||
dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
for (NSData *partData in dataArray) {
|
||||
__block NSData *immutablePartData = [partData copy];
|
||||
dispatch_data_t newDataPart =
|
||||
dispatch_data_create(immutablePartData.bytes, immutablePartData.length, bgQueue, ^{
|
||||
// We want the data retained until this block executes.
|
||||
immutablePartData = nil;
|
||||
});
|
||||
|
||||
if (dataAccumulator == nil) {
|
||||
// First part.
|
||||
dataAccumulator = newDataPart;
|
||||
} else {
|
||||
// Append the additional part.
|
||||
dataAccumulator = dispatch_data_create_concat(dataAccumulator, newDataPart);
|
||||
}
|
||||
}
|
||||
*outDispatchData = dataAccumulator;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSData *)dataWithHeaders:(NSDictionary *)headers {
|
||||
// Generate the header data by coalescing the dictionary as lines of "key: value\r\n".
|
||||
NSMutableString* headerString = [NSMutableString string];
|
||||
|
||||
// Sort the header keys so we have a deterministic order for unit testing.
|
||||
SEL sortSel = @selector(caseInsensitiveCompare:);
|
||||
NSArray *sortedKeys = [headers.allKeys sortedArrayUsingSelector:sortSel];
|
||||
|
||||
for (NSString *key in sortedKeys) {
|
||||
NSString *value = [headers objectForKey:key];
|
||||
|
||||
#if DEBUG
|
||||
// Look for troublesome characters in the header keys & values.
|
||||
NSCharacterSet *badKeyChars = [NSCharacterSet characterSetWithCharactersInString:@":\r\n"];
|
||||
NSCharacterSet *badValueChars = [NSCharacterSet characterSetWithCharactersInString:@"\r\n"];
|
||||
|
||||
NSRange badRange = [key rangeOfCharacterFromSet:badKeyChars];
|
||||
NSAssert(badRange.location == NSNotFound, @"invalid key: %@", key);
|
||||
|
||||
badRange = [value rangeOfCharacterFromSet:badValueChars];
|
||||
NSAssert(badRange.location == NSNotFound, @"invalid value: %@", value);
|
||||
#endif
|
||||
|
||||
[headerString appendFormat:@"%@: %@\r\n", key, value];
|
||||
}
|
||||
// Headers end with an extra blank line.
|
||||
[headerString appendString:@"\r\n"];
|
||||
|
||||
NSData *result = [headerString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark - Separating Parts
|
||||
|
||||
+ (NSArray *)MIMEPartsWithBoundary:(NSString *)boundary
|
||||
data:(NSData *)fullDocumentData {
|
||||
// In MIME documents, the boundary is preceded by CRLF and two dashes, and followed
|
||||
// at the end by two dashes.
|
||||
NSData *boundaryData = [boundary dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSUInteger boundaryLength = boundaryData.length;
|
||||
|
||||
NSMutableArray *foundBoundaryOffsets;
|
||||
[self searchData:fullDocumentData
|
||||
targetBytes:boundaryData.bytes
|
||||
targetLength:boundaryLength
|
||||
foundOffsets:&foundBoundaryOffsets];
|
||||
|
||||
// According to rfc1341, ignore anything before the first boundary, or after the last, though two
|
||||
// dashes are expected to follow the last boundary.
|
||||
if (foundBoundaryOffsets.count < 2) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Wrap the full document data with a dispatch_data_t for more efficient slicing
|
||||
// and dicing.
|
||||
dispatch_data_t dataWrapper;
|
||||
if ([fullDocumentData conformsToProtocol:@protocol(OS_dispatch_data)]) {
|
||||
dataWrapper = (dispatch_data_t)fullDocumentData;
|
||||
} else {
|
||||
// A no-op self invocation on fullDocumentData will keep it retained until the block is invoked.
|
||||
dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
dataWrapper = dispatch_data_create(fullDocumentData.bytes,
|
||||
fullDocumentData.length,
|
||||
bgQueue, ^{ [fullDocumentData self]; });
|
||||
}
|
||||
NSMutableArray *parts;
|
||||
NSInteger previousBoundaryOffset = -1;
|
||||
NSInteger partCounter = -1;
|
||||
NSInteger numberOfPartsWithHeaders = 0;
|
||||
for (NSNumber *currentBoundaryOffset in foundBoundaryOffsets) {
|
||||
++partCounter;
|
||||
if (previousBoundaryOffset == -1) {
|
||||
// This is the first boundary.
|
||||
previousBoundaryOffset = currentBoundaryOffset.integerValue;
|
||||
continue;
|
||||
} else {
|
||||
// Create a part data subrange between the previous boundary and this one.
|
||||
//
|
||||
// The last four bytes before a boundary are CRLF--.
|
||||
// The first two bytes following a boundary are either CRLF or, for the last boundary, --.
|
||||
NSInteger previousPartDataStartOffset =
|
||||
previousBoundaryOffset + (NSInteger)boundaryLength + 2;
|
||||
NSInteger previousPartDataEndOffset = currentBoundaryOffset.integerValue - 4;
|
||||
NSInteger previousPartDataLength = previousPartDataEndOffset - previousPartDataStartOffset;
|
||||
|
||||
if (previousPartDataLength < 2) {
|
||||
// The preceding part was too short to be useful.
|
||||
#if DEBUG
|
||||
NSLog(@"MIME part %ld has %ld bytes", (long)partCounter - 1,
|
||||
(long)previousPartDataLength);
|
||||
#endif
|
||||
} else {
|
||||
if (!parts) parts = [NSMutableArray array];
|
||||
|
||||
dispatch_data_t partData =
|
||||
dispatch_data_create_subrange(dataWrapper,
|
||||
(size_t)previousPartDataStartOffset, (size_t)previousPartDataLength);
|
||||
// Scan the part data for the separator between headers and body. After the CRLF,
|
||||
// either the headers start immediately, or there's another CRLF and there are no headers.
|
||||
//
|
||||
// We need to map the part data to get the first two bytes. (Or we could cast it to
|
||||
// NSData and get the bytes pointer of that.) If we're concerned that a single part
|
||||
// data may be expensive to map, we could make a subrange here for just the first two bytes,
|
||||
// and map that two-byte subrange.
|
||||
const void *partDataBuffer;
|
||||
size_t partDataBufferSize;
|
||||
dispatch_data_t mappedPartData NS_VALID_UNTIL_END_OF_SCOPE =
|
||||
dispatch_data_create_map(partData, &partDataBuffer, &partDataBufferSize);
|
||||
dispatch_data_t bodyData;
|
||||
NSDictionary *headers;
|
||||
BOOL hasAnotherCRLF = (((char *)partDataBuffer)[0] == '\r'
|
||||
&& ((char *)partDataBuffer)[1] == '\n');
|
||||
mappedPartData = nil;
|
||||
|
||||
if (hasAnotherCRLF) {
|
||||
// There are no headers; skip the CRLF to get to the body, and leave headers nil.
|
||||
bodyData = dispatch_data_create_subrange(partData, 2, (size_t)previousPartDataLength - 2);
|
||||
} else {
|
||||
// There are part headers. They are separated from body data by CRLFCRLF.
|
||||
NSArray *crlfOffsets;
|
||||
[self searchData:(NSData *)partData
|
||||
targetBytes:"\r\n\r\n"
|
||||
targetLength:4
|
||||
foundOffsets:&crlfOffsets];
|
||||
if (crlfOffsets.count == 0) {
|
||||
#if DEBUG
|
||||
// We could not distinguish body and headers.
|
||||
NSLog(@"MIME part %ld lacks a header separator: %@", (long)partCounter - 1,
|
||||
[[NSString alloc] initWithData:(NSData *)partData encoding:NSUTF8StringEncoding]);
|
||||
#endif
|
||||
} else {
|
||||
NSInteger headerSeparatorOffset = ((NSNumber *)crlfOffsets.firstObject).integerValue;
|
||||
dispatch_data_t headerData =
|
||||
dispatch_data_create_subrange(partData, 0, (size_t)headerSeparatorOffset);
|
||||
headers = [self headersWithData:(NSData *)headerData];
|
||||
|
||||
bodyData = dispatch_data_create_subrange(partData, (size_t)headerSeparatorOffset + 4,
|
||||
(size_t)(previousPartDataLength - (headerSeparatorOffset + 4)));
|
||||
|
||||
numberOfPartsWithHeaders++;
|
||||
} // crlfOffsets.count == 0
|
||||
} // hasAnotherCRLF
|
||||
GTMMIMEDocumentPart *part = [GTMMIMEDocumentPart partWithHeaders:headers
|
||||
body:(NSData *)bodyData];
|
||||
[parts addObject:part];
|
||||
} // previousPartDataLength < 2
|
||||
previousBoundaryOffset = currentBoundaryOffset.integerValue;
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
// In debug builds, warn if a reasonably long document lacks any CRLF characters.
|
||||
if (numberOfPartsWithHeaders == 0) {
|
||||
NSUInteger length = fullDocumentData.length;
|
||||
if (length > 20) { // Reasonably long.
|
||||
NSMutableArray *foundCRLFs;
|
||||
[self searchData:fullDocumentData
|
||||
targetBytes:"\r\n"
|
||||
targetLength:2
|
||||
foundOffsets:&foundCRLFs];
|
||||
if (foundCRLFs.count == 0) {
|
||||
// Parts were logged above (due to lacking header separators.)
|
||||
NSLog(@"Warning: MIME document lacks any headers (may have wrong line endings)");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // DEBUG
|
||||
return parts;
|
||||
}
|
||||
|
||||
// Efficiently search the supplied data for the target bytes.
|
||||
//
|
||||
// This uses enumerateByteRangesUsingBlock: to scan for bytes. It can find
|
||||
// the target even if it spans multiple separate byte ranges.
|
||||
//
|
||||
// Returns an array of found byte offset values, as NSNumbers.
|
||||
+ (void)searchData:(NSData *)data
|
||||
targetBytes:(const void *)targetBytes
|
||||
targetLength:(NSUInteger)targetLength
|
||||
foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets {
|
||||
NSMutableArray *foundOffsets = [NSMutableArray array];
|
||||
SearchDataForBytes(data, targetBytes, targetLength, foundOffsets, NULL);
|
||||
*outFoundOffsets = foundOffsets;
|
||||
}
|
||||
|
||||
|
||||
// This version of searchData: also returns the block numbers (0-based) where the
|
||||
// target was found, used for testing that the supplied dispatch_data buffer
|
||||
// has not been flattened.
|
||||
+ (void)searchData:(NSData *)data
|
||||
targetBytes:(const void *)targetBytes
|
||||
targetLength:(NSUInteger)targetLength
|
||||
foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets
|
||||
foundBlockNumbers:(GTM_NSArrayOf(NSNumber *) **)outFoundBlockNumbers {
|
||||
NSMutableArray *foundOffsets = [NSMutableArray array];
|
||||
NSMutableArray *foundBlockNumbers = [NSMutableArray array];
|
||||
|
||||
SearchDataForBytes(data, targetBytes, targetLength, foundOffsets, foundBlockNumbers);
|
||||
*outFoundOffsets = foundOffsets;
|
||||
*outFoundBlockNumbers = foundBlockNumbers;
|
||||
}
|
||||
|
||||
static void SearchDataForBytes(NSData *data, const void *targetBytes, NSUInteger targetLength,
|
||||
NSMutableArray *foundOffsets, NSMutableArray *foundBlockNumbers) {
|
||||
__block NSUInteger priorPartialMatchAmount = 0;
|
||||
__block NSInteger priorPartialMatchStartingBlockNumber = -1;
|
||||
__block NSInteger blockNumber = -1;
|
||||
|
||||
[data enumerateByteRangesUsingBlock:^(const void *bytes,
|
||||
NSRange byteRange,
|
||||
BOOL *stop) {
|
||||
// Search for the first character in the current range.
|
||||
const void *ptr = bytes;
|
||||
NSInteger remainingInCurrentRange = (NSInteger)byteRange.length;
|
||||
++blockNumber;
|
||||
|
||||
if (priorPartialMatchAmount > 0) {
|
||||
NSUInteger amountRemainingToBeMatched = targetLength - priorPartialMatchAmount;
|
||||
NSUInteger remainingFoundOffset;
|
||||
NSUInteger amountMatched = FindBytes(targetBytes + priorPartialMatchAmount,
|
||||
amountRemainingToBeMatched,
|
||||
ptr, (NSUInteger)remainingInCurrentRange, &remainingFoundOffset);
|
||||
if (amountMatched == 0 || remainingFoundOffset > 0) {
|
||||
// No match of the rest of the prior partial match in this range.
|
||||
} else if (amountMatched < amountRemainingToBeMatched) {
|
||||
// Another partial match; we're done with this range.
|
||||
priorPartialMatchAmount = priorPartialMatchAmount + amountMatched;
|
||||
return;
|
||||
} else {
|
||||
// The offset is in an earlier range.
|
||||
NSUInteger offset = byteRange.location - priorPartialMatchAmount;
|
||||
[foundOffsets addObject:@(offset)];
|
||||
[foundBlockNumbers addObject:@(priorPartialMatchStartingBlockNumber)];
|
||||
priorPartialMatchStartingBlockNumber = -1;
|
||||
}
|
||||
priorPartialMatchAmount = 0;
|
||||
}
|
||||
|
||||
while (remainingInCurrentRange > 0) {
|
||||
NSUInteger offsetFromPtr;
|
||||
NSUInteger amountMatched = FindBytes(targetBytes, targetLength, ptr,
|
||||
(NSUInteger)remainingInCurrentRange, &offsetFromPtr);
|
||||
if (amountMatched == 0) {
|
||||
// No match in this range.
|
||||
return;
|
||||
}
|
||||
if (amountMatched < targetLength) {
|
||||
// Found a partial target. If there's another range, we'll check for the rest.
|
||||
priorPartialMatchAmount = amountMatched;
|
||||
priorPartialMatchStartingBlockNumber = blockNumber;
|
||||
return;
|
||||
}
|
||||
// Found the full target.
|
||||
NSUInteger globalOffset = byteRange.location + (NSUInteger)(ptr - bytes) + offsetFromPtr;
|
||||
|
||||
[foundOffsets addObject:@(globalOffset)];
|
||||
[foundBlockNumbers addObject:@(blockNumber)];
|
||||
|
||||
ptr += targetLength + offsetFromPtr;
|
||||
remainingInCurrentRange -= (targetLength + offsetFromPtr);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// Internal method only for testing; this calls through the static method.
|
||||
+ (NSUInteger)findBytesWithNeedle:(const unsigned char *)needle
|
||||
needleLength:(NSUInteger)needleLength
|
||||
haystack:(const unsigned char *)haystack
|
||||
haystackLength:(NSUInteger)haystackLength
|
||||
foundOffset:(NSUInteger *)foundOffset {
|
||||
return FindBytes(needle, needleLength, haystack, haystackLength, foundOffset);
|
||||
}
|
||||
|
||||
// Utility method to parse header bytes into an NSDictionary.
|
||||
+ (NSDictionary *)headersWithData:(NSData *)data {
|
||||
NSString *headersString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
if (!headersString) return nil;
|
||||
|
||||
NSMutableDictionary *headers = [NSMutableDictionary dictionary];
|
||||
NSScanner *scanner = [NSScanner scannerWithString:headersString];
|
||||
// The scanner is skipping leading whitespace and newline characters by default.
|
||||
NSCharacterSet *newlineCharacters = [NSCharacterSet newlineCharacterSet];
|
||||
NSString *key;
|
||||
NSString *value;
|
||||
while ([scanner scanUpToString:@":" intoString:&key]
|
||||
&& [scanner scanString:@":" intoString:NULL]
|
||||
&& [scanner scanUpToCharactersFromSet:newlineCharacters intoString:&value]) {
|
||||
[headers setObject:value forKey:key];
|
||||
// Discard the trailing newline.
|
||||
[scanner scanCharactersFromSet:newlineCharacters intoString:NULL];
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// Return how much of the needle was found in the haystack.
|
||||
//
|
||||
// If the result is less than needleLen, then the beginning of the needle
|
||||
// was found at the end of the haystack.
|
||||
static NSUInteger FindBytes(const unsigned char* needle, NSUInteger needleLen,
|
||||
const unsigned char* haystack, NSUInteger haystackLen,
|
||||
NSUInteger *foundOffset) {
|
||||
const unsigned char *ptr = haystack;
|
||||
NSInteger remain = (NSInteger)haystackLen;
|
||||
// Assume memchr is an efficient way to find a match for the first
|
||||
// byte of the needle, and memcmp is an efficient way to compare a
|
||||
// range of bytes.
|
||||
while (remain > 0 && (ptr = memchr(ptr, needle[0], (size_t)remain)) != 0) {
|
||||
// The first character is present.
|
||||
NSUInteger offset = (NSUInteger)(ptr - haystack);
|
||||
remain = (NSInteger)(haystackLen - offset);
|
||||
|
||||
NSUInteger amountToCompare = MIN((NSUInteger)remain, needleLen);
|
||||
if (memcmp(ptr, needle, amountToCompare) == 0) {
|
||||
if (foundOffset) *foundOffset = offset;
|
||||
return amountToCompare;
|
||||
}
|
||||
ptr++;
|
||||
remain--;
|
||||
}
|
||||
if (foundOffset) *foundOffset = 0;
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user